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, } }