feat: add /search command to highlight matches in conversation
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 54s
CI / Lint & Test (pull_request) Failing after 5m52s
CI / Build Linux (pull_request) Has been skipped
CI / Build Windows (cross-compile) (pull_request) Has been skipped

Closes #32
This commit is contained in:
2026-01-21 20:19:46 -08:00
committed by Naomi Carrigan
parent 19ca7b7c6e
commit 59c7652f3e
4 changed files with 170 additions and 4 deletions
+47 -2
View File
@@ -6,9 +6,10 @@
interface Props {
content: string;
searchQuery?: string;
}
let { content }: Props = $props();
let { content, searchQuery = "" }: Props = $props();
let containerElement: HTMLDivElement;
const renderer = new marked.Renderer();
@@ -52,10 +53,47 @@
return processed;
}
function highlightSearchMatches(html: string, query: string): string {
if (!query) return html;
const codeBlockPlaceholders: string[] = [];
const tagPlaceholders: string[] = [];
// Temporarily replace code blocks with placeholders (don't highlight in code)
let processed = html.replace(/<(pre|code)[^>]*>[\s\S]*?<\/\1>/gi, (match) => {
codeBlockPlaceholders.push(match);
return `__CODE_SEARCH_PLACEHOLDER_${codeBlockPlaceholders.length - 1}__`;
});
// Temporarily replace all HTML tags with placeholders
processed = processed.replace(/<[^>]+>/g, (match) => {
tagPlaceholders.push(match);
return `__TAG_PLACEHOLDER_${tagPlaceholders.length - 1}__`;
});
// Apply search highlighting to text content
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(`(${escapedQuery})`, "gi");
processed = processed.replace(regex, '<mark class="search-highlight">$1</mark>');
// Restore HTML tags
processed = processed.replace(/__TAG_PLACEHOLDER_(\d+)__/g, (_, index) => {
return tagPlaceholders[parseInt(index)];
});
// Restore code blocks
processed = processed.replace(/__CODE_SEARCH_PLACEHOLDER_(\d+)__/g, (_, index) => {
return codeBlockPlaceholders[parseInt(index)];
});
return processed;
}
function renderMarkdown(text: string): string {
try {
const html = marked.parse(text) as string;
return processSpoilers(html);
const withSpoilers = processSpoilers(html);
return highlightSearchMatches(withSpoilers, searchQuery);
} catch {
return text;
}
@@ -304,4 +342,11 @@
color: var(--text-primary);
user-select: text;
}
.markdown-content :global(.search-highlight) {
background-color: var(--search-highlight, #fbbf24);
color: var(--search-highlight-text, #000);
border-radius: 2px;
padding: 0 2px;
}
</style>