b7addb5c7a
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
128 lines
3.7 KiB
TypeScript
128 lines
3.7 KiB
TypeScript
import { extractTextFromAst } from '~/utils/content-ast'
|
|
|
|
export interface PostSearchFilters {
|
|
title: boolean
|
|
description: boolean
|
|
content: boolean
|
|
tags: boolean
|
|
}
|
|
|
|
export interface PostSearchResult {
|
|
post: any
|
|
matchedLines: string[]
|
|
}
|
|
|
|
function parseQueryTerms(query: string): string[] {
|
|
const terms: string[] = []
|
|
const re = /"([^"]+)"|(\S+)/g
|
|
let m: RegExpExecArray | null
|
|
while ((m = re.exec(query)) !== null) {
|
|
const t = (m[1] ?? m[2] ?? '').trim().toLowerCase()
|
|
if (t) terms.push(t)
|
|
}
|
|
return terms
|
|
}
|
|
|
|
function highlightText(text: string, terms: string[]): string {
|
|
if (!terms.length) return text
|
|
const escaped = terms
|
|
.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
.sort((a, b) => b.length - a.length)
|
|
const regex = new RegExp(`(${escaped.join('|')})`, 'gi')
|
|
return text.replace(regex, '<mark class="search-highlight">$1</mark>')
|
|
}
|
|
|
|
function getMatchedLines(plainText: string, terms: string[]): string[] {
|
|
if (!terms.length || !plainText) return []
|
|
const lines = plainText.split('\n')
|
|
const matched: string[] = []
|
|
for (const line of lines) {
|
|
const lower = line.toLowerCase()
|
|
if (terms.every((t) => lower.includes(t))) {
|
|
const trimmed = line.trim()
|
|
if (
|
|
trimmed
|
|
&& trimmed.length > 10
|
|
&& !trimmed.startsWith('#')
|
|
&& !trimmed.startsWith('.')
|
|
&& !trimmed.includes('{')
|
|
&& !trimmed.includes('}')
|
|
&& !trimmed.includes(':')
|
|
&& !trimmed.includes(';')
|
|
&& !trimmed.match(/^[a-z][\w-]*[\s]*\{/)
|
|
&& !trimmed.match(/^\.shiki/)
|
|
&& !trimmed.match(/^var\(--/)
|
|
&& !trimmed.match(/^html\b/)
|
|
&& !trimmed.match(/^pre\b/)
|
|
&& !trimmed.match(/^@/)
|
|
) {
|
|
matched.push(trimmed)
|
|
}
|
|
}
|
|
}
|
|
return matched.slice(0, 10)
|
|
}
|
|
|
|
export function usePostSearch(
|
|
allPosts: Ref<any[] | null>,
|
|
searchQuery: Ref<string>,
|
|
filters: Ref<PostSearchFilters>,
|
|
) {
|
|
const searchResults = computed<PostSearchResult[]>(() => {
|
|
if (!allPosts.value || !searchQuery.value.trim()) {
|
|
return allPosts.value?.map((p) => ({ post: p, matchedLines: [] })) || []
|
|
}
|
|
|
|
const terms = parseQueryTerms(searchQuery.value)
|
|
if (!terms.length) return allPosts.value.map((p) => ({ post: p, matchedLines: [] }))
|
|
|
|
const results: PostSearchResult[] = []
|
|
|
|
for (const post of allPosts.value) {
|
|
const title = (post.title || '').toLowerCase()
|
|
const desc = (post.description || '').toLowerCase()
|
|
|
|
const matchTitle = filters.value.title && terms.some((t) => title.includes(t))
|
|
const matchDesc = filters.value.description && terms.some((t) => desc.includes(t))
|
|
const matchTags = filters.value.tags && (post.tags || []).some((tag: string) => terms.some((t) => tag.toLowerCase().includes(t)))
|
|
|
|
let matchContent = false
|
|
let matchedLines: string[] = []
|
|
if (filters.value.content) {
|
|
const plainText = extractTextFromAst(post.body?.value || post.body || null).toLowerCase()
|
|
matchContent = terms.some((t) => plainText.includes(t))
|
|
if (matchContent) {
|
|
matchedLines = getMatchedLines(
|
|
extractTextFromAst(post.body?.value || post.body || null),
|
|
terms,
|
|
)
|
|
}
|
|
}
|
|
|
|
if (matchTitle || matchDesc || matchContent || matchTags) {
|
|
results.push({ post, matchedLines })
|
|
}
|
|
}
|
|
|
|
return results
|
|
})
|
|
|
|
const filteredPosts = computed(() => searchResults.value.map((r) => r.post))
|
|
|
|
function highlight(text: string): string {
|
|
if (!searchQuery.value.trim()) return text
|
|
return highlightText(text, parseQueryTerms(searchQuery.value))
|
|
}
|
|
|
|
function getTerms(): string[] {
|
|
return parseQueryTerms(searchQuery.value)
|
|
}
|
|
|
|
return {
|
|
searchResults,
|
|
filteredPosts,
|
|
highlight,
|
|
getTerms,
|
|
}
|
|
}
|