import type { PlaitElement } from "@plait/core"; import { getTopicEntries } from "./topic-details"; export type TopicSearchResult = { id: string; title: string; sheetId: string; sheetTitle: string; occurrences: number }; export type SearchSheet = { id: string; title: string; children: PlaitElement[] }; export function searchTopics(sheets: SearchSheet[], query: string, caseSensitive = false): TopicSearchResult[] { if (!query) return []; const needle = caseSensitive ? query : query.toLocaleLowerCase(); return sheets.flatMap(sheet => getTopicEntries(sheet.children).flatMap(({ element, title }) => { const text = caseSensitive ? title : title.toLocaleLowerCase(); let count = 0; let offset = 0; for (let at = text.indexOf(needle, offset); at !== -1; at = text.indexOf(needle, offset)) { count++; offset = at + needle.length; } return count ? [{ id: element.id, title, sheetId: sheet.id, sheetTitle: sheet.title, occurrences: count }] : []; })); } /** A match may cross styled leaves. Replacement inherits the first matched leaf's marks. */ export function replaceTopicText(element: PlaitElement, query: string, replacement: string, caseSensitive = false) { const topic = element.data?.topic; if (!query || !topic) return topic; const leaves: { node: Record; start: number; end: number }[] = []; let source = ""; const collect = (node: Record) => { if (typeof node.text === "string") { const start = source.length; source += node.text; leaves.push({ node, start, end: source.length }); } else if (Array.isArray(node.children)) { node.children.forEach(child => collect(child as Record)); } }; collect(topic); const pattern = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), caseSensitive ? "g" : "gi"); const matches = Array.from(source.matchAll(pattern), match => ({ start: match.index!, end: match.index! + match[0].length })); const replacements = new Map(); for (const { node, start, end } of leaves) { let cursor = start; let text = ""; for (const match of matches) { if (match.end <= start || match.start >= end) continue; text += source.slice(cursor, Math.max(cursor, match.start)); if (match.start >= start) text += replacement; cursor = Math.min(end, match.end); } replacements.set(node, text + source.slice(cursor, end)); } const visit = (node: Record): Record => { if (typeof node.text === "string") return { ...node, text: replacements.get(node) ?? node.text }; if (Array.isArray(node.children)) return { ...node, children: node.children.map(child => visit(child as Record)) }; return node; }; return visit(topic); }