103 lines
2.8 KiB
TypeScript
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)
|
|
}
|