Phase 05: Search - 3 plans in 2 waves - Wave 1: 05-01 (UI components), 05-02 (filtering logic) — parallel - Wave 2: 05-03 (integration + recent searches + "/" shortcut) - Ready for execution Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
6.3 KiB
6.3 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 05-search | 02 | execute | 1 |
|
true |
|
Purpose: Implement pure functions that filter entries based on SearchFilters criteria and highlight matching text. These are decoupled from UI components for testability. Output: filterEntries.ts and highlightText.ts utilities ready for use in the integrated search view.
<execution_context> @/home/tho/.claude/get-shit-done/workflows/execute-plan.md @/home/tho/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/05-search/05-CONTEXT.md @.planning/phases/05-search/05-RESEARCH.md @src/lib/server/db/schema.ts Task 1: Create filterEntries utility function src/lib/utils/filterEntries.ts Create filterEntries.ts with a pure function that filters entries:import type { SearchFilters } from '$lib/types/search';
import type { Entry, Tag } from '$lib/server/db/schema';
interface EntryWithTags extends Entry {
tags: Tag[];
}
export function filterEntries(
entries: EntryWithTags[],
filters: SearchFilters
): EntryWithTags[] {
let result = entries;
// Text search (title + content) - minimum 2 characters
if (filters.query.length >= 2) {
const query = filters.query.toLowerCase();
result = result.filter(e =>
(e.title?.toLowerCase().includes(query)) ||
e.content.toLowerCase().includes(query)
);
}
// Tag filter - AND logic (must have ALL selected tags)
if (filters.tags.length > 0) {
result = result.filter(e =>
filters.tags.every(tagName =>
e.tags.some(t => t.name.toLowerCase() === tagName.toLowerCase())
)
);
}
// Type filter
if (filters.type !== 'all') {
result = result.filter(e => e.type === filters.type);
}
// Date range filter (using createdAt)
if (filters.dateRange.start) {
result = result.filter(e => e.createdAt >= filters.dateRange.start!);
}
if (filters.dateRange.end) {
// Include the end date by comparing to end of day
const endOfDay = filters.dateRange.end + 'T23:59:59';
result = result.filter(e => e.createdAt <= endOfDay);
}
return result;
}
Implementation notes:
- Case-insensitive text search (lowercase comparison)
- Case-insensitive tag matching (tags stored with original case but matched insensitively per 04-01 decision)
- AND logic for tags (every selected tag must be present)
- Date comparison works with ISO strings (schema uses ISO format)
- End date is inclusive (compare to end of day)
TypeScript compiles:
npx tsc --noEmitfilterEntries function filters entries by query, tags, type, and date range
/**
* Wraps matching text in <mark> tags for highlighting.
* Returns HTML string to be used with {@html} in Svelte.
*
* @param text - The text to search within
* @param query - The search query to highlight
* @returns HTML string with matches wrapped in <mark> tags
*/
export function highlightText(text: string, query: string): string {
// Don't highlight if query is too short
if (!query || query.length < 2) return escapeHtml(text);
// Escape HTML in the original text first
const escaped = escapeHtml(text);
// Escape regex special characters in query
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escapedQuery})`, 'gi');
// Wrap matches in <mark> with bold styling (no background per CONTEXT.md)
return escaped.replace(regex, '<mark class="font-bold bg-transparent">$1</mark>');
}
/**
* Escapes HTML special characters to prevent XSS.
*/
function escapeHtml(text: string): string {
const map: Record<string, string> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, char => map[char]);
}
Security notes:
- Always escape HTML before highlighting to prevent XSS
- The escapeHtml function runs BEFORE regex replacement
- Result is safe to use with {@html} directive
Styling notes:
- Uses font-bold for emphasis (per CONTEXT.md: "bold, not background highlight")
- bg-transparent ensures no background color on
TypeScript compiles:
npx tsc --noEmithighlightText function wraps matching text in styled mark tags with XSS protection
<success_criteria>
- filterEntries handles all filter types (query, tags, type, dateRange)
- Tag filtering uses AND logic (entry must have ALL selected tags)
- Text search is case-insensitive with 2-character minimum
- highlightText escapes HTML to prevent XSS
- Highlighted text uses bold styling without background </success_criteria>