Files
planet/frontend/src/pages/Docs/docs-search.ts
2026-04-28 04:27:18 +08:00

103 lines
2.8 KiB
TypeScript

import type { DocsEntry, DocsHeading } from './docs-content'
import { extractHeadings } from './docs-content'
export interface DocsSearchRecord {
entry: DocsEntry
markdown: string
headings: DocsHeading[]
plainText: string
}
export interface DocsSearchResult {
entry: DocsEntry
score: number
excerpt: string
}
const DEFAULT_EXCERPT_LENGTH = 160
const EXCERPT_CONTEXT_BEFORE = 56
const EXCERPT_CONTEXT_AFTER = 104
const MAX_SEARCH_RESULTS = 12
const SEARCH_SCORE = {
title: 80,
slug: 36,
group: 24,
headings: 32,
body: 8,
}
function stripMarkdown(markdown: string): string {
return markdown
.replace(/```[\s\S]*?```/g, ' ')
.replace(/`([^`]+)`/g, '$1')
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
.replace(/[#>*_\-|]/g, ' ')
.replace(/\s+/g, ' ')
.trim()
}
function createExcerpt(text: string, query: string): string {
const lowerText = text.toLowerCase()
const lowerQuery = query.toLowerCase()
const matchIndex = lowerText.indexOf(lowerQuery)
if (matchIndex < 0) {
return text.slice(0, DEFAULT_EXCERPT_LENGTH)
}
const start = Math.max(0, matchIndex - EXCERPT_CONTEXT_BEFORE)
const end = Math.min(text.length, matchIndex + query.length + EXCERPT_CONTEXT_AFTER)
const prefix = start > 0 ? '...' : ''
const suffix = end < text.length ? '...' : ''
return `${prefix}${text.slice(start, end)}${suffix}`
}
export async function buildDocsSearchRecords(entries: DocsEntry[]): Promise<DocsSearchRecord[]> {
const records = await Promise.all(
entries.map(async (entry) => {
const markdown = await entry.loader()
return {
entry,
markdown,
headings: extractHeadings(markdown),
plainText: stripMarkdown(markdown),
}
}),
)
return records
}
export function searchDocs(records: DocsSearchRecord[], rawQuery: string): DocsSearchResult[] {
const query = rawQuery.trim().toLowerCase()
if (!query) {
return []
}
return records
.map((record) => {
const title = record.entry.title.toLowerCase()
const slug = record.entry.slug.toLowerCase()
const group = record.entry.group.toLowerCase()
const headings = record.headings.map((heading) => heading.text).join(' ').toLowerCase()
const body = record.plainText.toLowerCase()
let score = 0
if (title.includes(query)) score += SEARCH_SCORE.title
if (slug.includes(query)) score += SEARCH_SCORE.slug
if (group.includes(query)) score += SEARCH_SCORE.group
if (headings.includes(query)) score += SEARCH_SCORE.headings
if (body.includes(query)) score += SEARCH_SCORE.body
return {
entry: record.entry,
score,
excerpt: createExcerpt(record.plainText, rawQuery.trim()),
}
})
.filter((result) => result.score > 0)
.sort((a, b) => b.score - a.score || a.entry.order - b.entry.order)
.slice(0, MAX_SEARCH_RESULTS)
}