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, '$1')
}
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,
searchQuery: Ref,
filters: Ref,
) {
const searchResults = computed(() => {
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,
}
}