refactor: consolidate note-list body search
This commit is contained in:
@@ -771,7 +771,7 @@ interface SearchResult {
|
||||
- Click result to open note in editor
|
||||
- Shows relevance score and snippet
|
||||
|
||||
The NoteList header search keeps its local title/snippet/property filtering for immediate scoped results, then augments the match set with `search_vault` full-content hits from the visible workspace roots. It verifies backend hits against Markdown body text with frontmatter excluded, and stores only matching paths so body-only matches appear in the current list scope without rendering private matched text in note rows.
|
||||
The NoteList header search keeps its local title/snippet/property filtering for immediate scoped results, then augments the match set with `search_vault` hits from the visible workspace roots using the command's frontmatter-excluding search option. React stores only matching paths so body-only matches appear in the current list scope without a second content-read pass or rendering private matched text in note rows.
|
||||
|
||||
No indexing step required — search runs directly against the filesystem.
|
||||
|
||||
|
||||
@@ -454,7 +454,7 @@ Search is keyword-based, using `walkdir` to scan all `.md` files in the vault di
|
||||
|
||||
The `search_vault` Tauri command runs the scan in a blocking Tokio task and returns results sorted by relevance score.
|
||||
|
||||
The note-list search field combines client-side scoped filtering with that same command: title, snippet, and visible-property matches resolve immediately, while backend full-content hits are verified against Markdown body text, excluding frontmatter, before adding matching paths for the currently visible workspace roots without displaying matched body text in the note row.
|
||||
The note-list search field combines client-side scoped filtering with that same command: title, snippet, and visible-property matches resolve immediately, while backend body-content hits use `search_vault` with frontmatter excluded before adding matching paths for the currently visible workspace roots without displaying matched body text in the note row.
|
||||
|
||||
## Vault Cache System
|
||||
|
||||
|
||||
@@ -100,14 +100,24 @@ pub async fn reload_vault(
|
||||
pub async fn search_vault(
|
||||
vault_path: String,
|
||||
query: String,
|
||||
mode: String,
|
||||
limit: Option<usize>,
|
||||
exclude_frontmatter: Option<bool>,
|
||||
) -> Result<SearchResponse, String> {
|
||||
let vault_path = expand_tilde(&vault_path).into_owned();
|
||||
let limit = limit.unwrap_or(20);
|
||||
tokio::task::spawn_blocking(move || search::search_vault(&vault_path, &query, &mode, limit))
|
||||
.await
|
||||
.map_err(|e| format!("Search task failed: {}", e))?
|
||||
let exclude_frontmatter = exclude_frontmatter.unwrap_or(false);
|
||||
tokio::task::spawn_blocking(move || {
|
||||
search::search_vault_with_options(search::SearchOptions {
|
||||
vault_path: &vault_path,
|
||||
query: &query,
|
||||
mode: "keyword",
|
||||
limit,
|
||||
hide_gitignored_files: crate::settings::hide_gitignored_files_enabled(),
|
||||
exclude_frontmatter,
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("Search task failed: {}", e))?
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -264,7 +274,7 @@ mod tests {
|
||||
let response = search_vault(
|
||||
dir.path().to_string_lossy().into_owned(),
|
||||
"needle".to_string(),
|
||||
"keyword".to_string(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
@@ -287,8 +297,8 @@ mod tests {
|
||||
let response = search_vault(
|
||||
dir.path().to_string_lossy().into_owned(),
|
||||
"needle".to_string(),
|
||||
"keyword".to_string(),
|
||||
Some(1),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -26,6 +26,7 @@ pub struct SearchOptions<'a> {
|
||||
pub mode: &'a str,
|
||||
pub limit: usize,
|
||||
pub hide_gitignored_files: bool,
|
||||
pub exclude_frontmatter: bool,
|
||||
}
|
||||
|
||||
struct Utf8Boundary<'a> {
|
||||
@@ -126,9 +127,29 @@ pub fn search_vault(
|
||||
mode: _mode,
|
||||
limit,
|
||||
hide_gitignored_files: crate::settings::hide_gitignored_files_enabled(),
|
||||
exclude_frontmatter: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn strip_frontmatter(content: &str) -> &str {
|
||||
let Some(rest) = content.strip_prefix("---") else {
|
||||
return content;
|
||||
};
|
||||
|
||||
match rest.find("\n---") {
|
||||
Some(end) => rest[end + 4..].trim_start(),
|
||||
None => content,
|
||||
}
|
||||
}
|
||||
|
||||
fn searchable_content(content: &str, exclude_frontmatter: bool) -> &str {
|
||||
if exclude_frontmatter {
|
||||
strip_frontmatter(content)
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
|
||||
fn is_markdown_search_candidate(vault_dir: &Path, path: &Path) -> bool {
|
||||
if !path.extension().is_some_and(|ext| ext == "md") {
|
||||
return false;
|
||||
@@ -164,7 +185,8 @@ pub fn search_vault_with_options(options: SearchOptions<'_>) -> Result<SearchRes
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let content_lower = content.to_lowercase();
|
||||
let searchable_content = searchable_content(&content, options.exclude_frontmatter);
|
||||
let content_lower = searchable_content.to_lowercase();
|
||||
let filename = path
|
||||
.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
@@ -183,7 +205,7 @@ pub fn search_vault_with_options(options: SearchOptions<'_>) -> Result<SearchRes
|
||||
}
|
||||
.score();
|
||||
let snippet = SnippetRequest {
|
||||
content: &content,
|
||||
content: searchable_content,
|
||||
query_lower: &query_lower,
|
||||
}
|
||||
.extract();
|
||||
@@ -364,6 +386,7 @@ mod tests {
|
||||
mode: "keyword",
|
||||
limit: 10,
|
||||
hide_gitignored_files: true,
|
||||
exclude_frontmatter: false,
|
||||
})
|
||||
.unwrap();
|
||||
let shown = search_vault_with_options(SearchOptions {
|
||||
@@ -372,6 +395,7 @@ mod tests {
|
||||
mode: "keyword",
|
||||
limit: 10,
|
||||
hide_gitignored_files: false,
|
||||
exclude_frontmatter: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -379,4 +403,44 @@ mod tests {
|
||||
assert_eq!(hidden.results[0].title, "Visible");
|
||||
assert_eq!(shown.results.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_vault_can_exclude_frontmatter_from_content_matches() {
|
||||
let dir = Builder::new()
|
||||
.prefix("search-frontmatter-scope-")
|
||||
.tempdir_in(std::env::current_dir().unwrap())
|
||||
.unwrap();
|
||||
fs::write(
|
||||
dir.path().join("frontmatter-only.md"),
|
||||
[
|
||||
"---",
|
||||
"Owner: hidden-frontmatter-keyword",
|
||||
"---",
|
||||
"",
|
||||
"# Public Body",
|
||||
"",
|
||||
"The note body deliberately omits the hidden property token.",
|
||||
]
|
||||
.join("\n"),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
dir.path().join("body-match.md"),
|
||||
"# Body Match\n\nBody includes hidden-frontmatter-keyword here.",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let response = search_vault_with_options(SearchOptions {
|
||||
vault_path: dir.path().to_str().unwrap(),
|
||||
query: "hidden-frontmatter-keyword",
|
||||
mode: "keyword",
|
||||
limit: 10,
|
||||
hide_gitignored_files: false,
|
||||
exclude_frontmatter: true,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.results.len(), 1);
|
||||
assert_eq!(response.results[0].title, "Body Match");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,10 +118,8 @@ interface NoteListSearchMockResult {
|
||||
}
|
||||
|
||||
function installFullTextSearchMocks({
|
||||
contentByPath,
|
||||
resultsByVault,
|
||||
}: {
|
||||
contentByPath: Record<string, string>
|
||||
resultsByVault: Record<string, NoteListSearchMockResult[]>
|
||||
}) {
|
||||
const originalContentHandler = window.__mockHandlers?.get_note_content
|
||||
@@ -132,7 +130,9 @@ function installFullTextSearchMocks({
|
||||
query: args?.query,
|
||||
results: resultsByVault[String(args?.vaultPath ?? '')] ?? [],
|
||||
}))
|
||||
const getNoteContent = vi.fn((args?: Record<string, unknown>) => contentByPath[String(args?.path ?? '')] ?? '')
|
||||
const getNoteContent = vi.fn(() => {
|
||||
throw new Error('Note-list full-text search should not read note content in React')
|
||||
})
|
||||
|
||||
if (!window.__mockHandlers) window.__mockHandlers = {}
|
||||
window.__mockHandlers.search_vault = searchVault
|
||||
@@ -291,10 +291,7 @@ describe('NoteList rendering', () => {
|
||||
})
|
||||
|
||||
it('filters by full note content when the title and snippet do not match', async () => {
|
||||
const { restore, searchVault } = installFullTextSearchMocks({
|
||||
contentByPath: {
|
||||
'/vault/b.md': '# Beta Note\n\nA private subterranean-keyword body match.',
|
||||
},
|
||||
const { getNoteContent, restore, searchVault } = installFullTextSearchMocks({
|
||||
resultsByVault: {
|
||||
'/vault': [{
|
||||
note_type: 'Note',
|
||||
@@ -321,8 +318,10 @@ describe('NoteList rendering', () => {
|
||||
vaultPath: '/vault',
|
||||
query: 'subterranean-keyword',
|
||||
mode: 'keyword',
|
||||
excludeFrontmatter: true,
|
||||
}))
|
||||
})
|
||||
expect(getNoteContent).not.toHaveBeenCalled()
|
||||
expect(screen.getByText('Beta Note')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Alpha Note')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Private body match is intentionally not rendered here.')).not.toBeInTheDocument()
|
||||
@@ -332,26 +331,9 @@ describe('NoteList rendering', () => {
|
||||
})
|
||||
|
||||
it('ignores full-content matches that only appear in hidden frontmatter', async () => {
|
||||
const { restore } = installFullTextSearchMocks({
|
||||
contentByPath: {
|
||||
'/vault/a.md': [
|
||||
'---',
|
||||
'Owner: hidden-frontmatter-keyword',
|
||||
'---',
|
||||
'',
|
||||
'# Alpha Note',
|
||||
'',
|
||||
'Public body text omits the private property value.',
|
||||
].join('\n'),
|
||||
},
|
||||
const { getNoteContent, restore, searchVault } = installFullTextSearchMocks({
|
||||
resultsByVault: {
|
||||
'/vault': [{
|
||||
note_type: 'Note',
|
||||
path: '/vault/a.md',
|
||||
score: 1,
|
||||
snippet: 'Owner: hidden-frontmatter-keyword',
|
||||
title: 'Alpha Note',
|
||||
}],
|
||||
'/vault': [],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -365,6 +347,12 @@ describe('NoteList rendering', () => {
|
||||
|
||||
await searchNoteList('hidden-frontmatter-keyword')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(searchVault).toHaveBeenCalledWith(expect.objectContaining({
|
||||
excludeFrontmatter: true,
|
||||
}))
|
||||
})
|
||||
expect(getNoteContent).not.toHaveBeenCalled()
|
||||
expect(screen.queryByText('Alpha Note')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('No matching notes')).toBeInTheDocument()
|
||||
} finally {
|
||||
@@ -373,11 +361,7 @@ describe('NoteList rendering', () => {
|
||||
})
|
||||
|
||||
it('runs full-content note-list search across visible workspaces', async () => {
|
||||
const { restore, searchVault } = installFullTextSearchMocks({
|
||||
contentByPath: {
|
||||
'/personal/personal-note.md': '# Personal Note\n\nNo matching body token.',
|
||||
'/team/team-body-hit.md': '# Team Body Hit\n\nPrivate workspace-only-keyword body hit.',
|
||||
},
|
||||
const { getNoteContent, restore, searchVault } = installFullTextSearchMocks({
|
||||
resultsByVault: {
|
||||
'/team': [{
|
||||
note_type: 'Note',
|
||||
@@ -412,9 +396,10 @@ describe('NoteList rendering', () => {
|
||||
await searchNoteList('workspace-only-keyword')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(searchVault).toHaveBeenCalledWith(expect.objectContaining({ vaultPath: '/personal' }))
|
||||
expect(searchVault).toHaveBeenCalledWith(expect.objectContaining({ vaultPath: '/team' }))
|
||||
expect(searchVault).toHaveBeenCalledWith(expect.objectContaining({ vaultPath: '/personal', excludeFrontmatter: true }))
|
||||
expect(searchVault).toHaveBeenCalledWith(expect.objectContaining({ vaultPath: '/team', excludeFrontmatter: true }))
|
||||
})
|
||||
expect(getNoteContent).not.toHaveBeenCalled()
|
||||
expect(screen.getByText('Team Body Hit')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Personal Note')).not.toBeInTheDocument()
|
||||
} finally {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { invoke } from '@tauri-apps/api/core'
|
||||
import type { VaultEntry } from '../../types'
|
||||
import { isTauri, mockInvoke } from '../../mock-tauri'
|
||||
|
||||
type MarkdownContent = string
|
||||
type NoteListSearchQuery = string
|
||||
type NotePath = string
|
||||
type VaultPath = string
|
||||
@@ -22,6 +21,7 @@ interface SearchCommandArgs extends Record<string, unknown> {
|
||||
query: NoteListSearchQuery
|
||||
mode: 'keyword'
|
||||
limit: number
|
||||
excludeFrontmatter: true
|
||||
}
|
||||
|
||||
interface FullTextSearchRequest {
|
||||
@@ -85,51 +85,14 @@ function resolveNoteListSearchVaultPaths(entries: VaultEntry[]): VaultPath[] {
|
||||
return root ? [root] : []
|
||||
}
|
||||
|
||||
function stripFrontmatter(content: MarkdownContent): MarkdownContent {
|
||||
const lineEnding = content.startsWith('---\r\n')
|
||||
? '\r\n'
|
||||
: content.startsWith('---\n') ? '\n' : null
|
||||
if (!lineEnding) return content
|
||||
|
||||
const afterOpen = content.slice(3 + lineEnding.length)
|
||||
const closeMarker = `${lineEnding}---`
|
||||
const closeIndex = afterOpen.indexOf(closeMarker)
|
||||
if (closeIndex === -1) return content
|
||||
|
||||
return stripFrontmatterCloseLine(afterOpen.slice(closeIndex + closeMarker.length))
|
||||
}
|
||||
|
||||
function stripFrontmatterCloseLine(content: MarkdownContent): MarkdownContent {
|
||||
if (content.startsWith('\r\n')) return content.slice(2)
|
||||
return content.startsWith('\n') ? content.slice(1) : content
|
||||
}
|
||||
|
||||
function searchVault(args: SearchCommandArgs): Promise<SearchResponseData> {
|
||||
return isTauri()
|
||||
? invoke<SearchResponseData>('search_vault', args)
|
||||
: mockInvoke<SearchResponseData>('search_vault', args)
|
||||
}
|
||||
|
||||
function readNoteContent(path: NotePath): Promise<MarkdownContent> {
|
||||
return isTauri()
|
||||
? invoke<MarkdownContent>('get_note_content', { path })
|
||||
: mockInvoke<MarkdownContent>('get_note_content', { path })
|
||||
}
|
||||
|
||||
async function bodyContainsQuery({ path, query }: { path: NotePath; query: NoteListSearchQuery }): Promise<boolean> {
|
||||
try {
|
||||
const body = stripFrontmatter(await readNoteContent(path))
|
||||
return body.toLowerCase().includes(query)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveBodyResultPaths({ paths, query }: { paths: NotePath[]; query: NoteListSearchQuery }): Promise<Set<string>> {
|
||||
const matches = await Promise.all(unique(paths).map(async (path) => (
|
||||
await bodyContainsQuery({ path, query }) ? path : null
|
||||
)))
|
||||
return new Set(matches.filter((path): path is string => !!path))
|
||||
function resolveResultPaths(responses: SearchResponseData[]): Set<string> {
|
||||
return new Set(unique(responses.flatMap((response) => response.results.map((result) => result.path))))
|
||||
}
|
||||
|
||||
async function runFullTextSearch(request: FullTextSearchRequest): Promise<Set<string>> {
|
||||
@@ -138,11 +101,9 @@ async function runFullTextSearch(request: FullTextSearchRequest): Promise<Set<st
|
||||
query: request.query,
|
||||
mode: 'keyword',
|
||||
limit: request.limit,
|
||||
excludeFrontmatter: true,
|
||||
})))
|
||||
return resolveBodyResultPaths({
|
||||
paths: responses.flatMap((response) => response.results.map((result) => result.path)),
|
||||
query: request.query,
|
||||
})
|
||||
return resolveResultPaths(responses)
|
||||
}
|
||||
|
||||
function createSearchRequest({ entries, query }: { entries: VaultEntry[]; query: NoteListSearchQuery }): FullTextSearchRequest | null {
|
||||
|
||||
@@ -34,6 +34,23 @@ function mockFileHistory(path: string) {
|
||||
]
|
||||
}
|
||||
|
||||
function stripMockFrontmatter(content: string): string {
|
||||
const lineEnding = content.startsWith('---\r\n')
|
||||
? '\r\n'
|
||||
: content.startsWith('---\n') ? '\n' : null
|
||||
if (!lineEnding) return content
|
||||
|
||||
const afterOpen = content.slice(3 + lineEnding.length)
|
||||
const closeIndex = afterOpen.indexOf(`${lineEnding}---`)
|
||||
if (closeIndex === -1) return content
|
||||
|
||||
return afterOpen.slice(closeIndex + lineEnding.length + 3).trimStart()
|
||||
}
|
||||
|
||||
function mockSearchContent(content: string, excludeFrontmatter?: boolean): string {
|
||||
return excludeFrontmatter ? stripMockFrontmatter(content) : content
|
||||
}
|
||||
|
||||
function mockModifiedFiles(): ModifiedFile[] {
|
||||
return [
|
||||
{ path: '/Users/luca/Laputa/26q1-laputa-app.md', relativePath: '26q1-laputa-app.md', status: 'modified' },
|
||||
@@ -563,12 +580,12 @@ export const mockHandlers: Record<string, (args: any) => any> = {
|
||||
migrate_is_a_to_type: () => 0,
|
||||
batch_archive_notes: (args: { paths: string[] }) => args.paths.length,
|
||||
batch_trash_notes: (args: { paths: string[] }) => args.paths.length,
|
||||
search_vault: (args: { query: string; mode: string }) => {
|
||||
search_vault: (args: { query: string; mode: string; excludeFrontmatter?: boolean }) => {
|
||||
const q = (args.query ?? '').toLowerCase()
|
||||
if (!q) return { results: [], elapsed_ms: 0, query: q, mode: args.mode }
|
||||
const matches = MOCK_ENTRIES
|
||||
.filter(e => {
|
||||
const content = MOCK_CONTENT[e.path] ?? ''
|
||||
const content = mockSearchContent(MOCK_CONTENT[e.path] ?? '', args.excludeFrontmatter)
|
||||
return e.title.toLowerCase().includes(q) || content.toLowerCase().includes(q)
|
||||
})
|
||||
.slice(0, 20)
|
||||
|
||||
@@ -95,7 +95,9 @@ function buildSearchRequest(args: Record<string, unknown>): VaultApiRequest | nu
|
||||
if (!query || !lastVaultPath) return null
|
||||
|
||||
const mode = argText(payload, 'mode') ?? 'all'
|
||||
return { kind: 'search', body: { mode, query, vault_path: lastVaultPath } }
|
||||
const body: Record<string, unknown> = { mode, query, vault_path: lastVaultPath }
|
||||
if (Reflect.get(payload, 'excludeFrontmatter') === true) body.exclude_frontmatter = true
|
||||
return { kind: 'search', body }
|
||||
}
|
||||
|
||||
function isPathQueryCommand(cmd: string): cmd is PathQueryCommand {
|
||||
|
||||
@@ -336,8 +336,11 @@ async function installFixtureVaultInitScript({ page, vaultPath, isGitRepo, folde
|
||||
)
|
||||
const query = encodeURIComponent(readCommandString(commandArgs, 'query'))
|
||||
const mode = encodeURIComponent(readCommandString(commandArgs, 'mode', 'all'))
|
||||
const excludeFrontmatter = readCommandValue(commandArgs, 'excludeFrontmatter') === true
|
||||
? '&exclude_frontmatter=1'
|
||||
: ''
|
||||
return readJson(
|
||||
`/api/vault/search?vault_path=${encodeURIComponent(resolvedPath)}&query=${query}&mode=${mode}`,
|
||||
`/api/vault/search?vault_path=${encodeURIComponent(resolvedPath)}&query=${query}&mode=${mode}${excludeFrontmatter}`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -89,12 +89,14 @@ type FrontmatterPropertyValue = string | number | boolean | null
|
||||
type VaultSearchResult = { title: string; path: string; snippet: string; score: number; note_type: string | null }
|
||||
|
||||
interface SearchEntryInput {
|
||||
excludeFrontmatter: boolean
|
||||
entry: VaultEntry
|
||||
query: string
|
||||
rawContent: string
|
||||
}
|
||||
|
||||
interface SearchRequestInput {
|
||||
excludeFrontmatter: boolean
|
||||
query: string
|
||||
vaultPath: string
|
||||
}
|
||||
@@ -411,6 +413,11 @@ function commandString({ args, key }: CommandStringInput): string | null {
|
||||
return typeof value === 'string' && value.length > 0 ? value : null
|
||||
}
|
||||
|
||||
function commandBool({ args, key }: CommandStringInput): boolean {
|
||||
const value = Reflect.get(args, key)
|
||||
return value === true || value === '1' || value === 'true'
|
||||
}
|
||||
|
||||
function commandResponse({ payload, statusCode = 200 }: CommandResponseInput): VaultCommandResponse {
|
||||
return { payload, statusCode }
|
||||
}
|
||||
@@ -463,7 +470,10 @@ function commandVaultSearch({ args }: VaultCommandContext): VaultCommandResponse
|
||||
const vaultPath = commandString({ args, key: 'vault_path' })
|
||||
const query = (commandString({ args, key: 'query' }) ?? '').toLowerCase()
|
||||
const mode = commandString({ args, key: 'mode' }) ?? 'all'
|
||||
const results = vaultPath && query ? collectVaultSearchResults({ vaultPath, query }) : []
|
||||
const excludeFrontmatter = commandBool({ args, key: 'exclude_frontmatter' })
|
||||
const results = vaultPath && query
|
||||
? collectVaultSearchResults({ vaultPath, query, excludeFrontmatter })
|
||||
: []
|
||||
return commandResponse({ payload: { results, elapsed_ms: results.length > 0 ? 1 : 0, query, mode } })
|
||||
}
|
||||
|
||||
@@ -548,7 +558,7 @@ function vaultCommandContext(payload: VaultCommandPayload): VaultCommandContext
|
||||
return { cmd: payload.cmd, args: payload.args }
|
||||
}
|
||||
|
||||
const VAULT_ENDPOINT_ARG_KEYS = ['path', 'vault_path', 'query', 'mode', 'reload'] as const
|
||||
const VAULT_ENDPOINT_ARG_KEYS = ['path', 'vault_path', 'query', 'mode', 'reload', 'exclude_frontmatter'] as const
|
||||
|
||||
function readVaultQueryArgs(url: URL): Record<string, unknown> {
|
||||
const args: Record<string, unknown> = {}
|
||||
@@ -650,19 +660,26 @@ async function handleVaultSearch(url: URL, req: IncomingMessage, res: ServerResp
|
||||
return handleVaultReadCommand({ cmd: 'search_vault', pathname: '/api/vault/search', req, res, url })
|
||||
}
|
||||
|
||||
function collectVaultSearchResults({ vaultPath, query }: SearchRequestInput): VaultSearchResult[] {
|
||||
function collectVaultSearchResults({ excludeFrontmatter, vaultPath, query }: SearchRequestInput): VaultSearchResult[] {
|
||||
const results: VaultSearchResult[] = []
|
||||
for (const filePath of findMarkdownFiles(vaultPath)) {
|
||||
const entry = parseMarkdownFile(filePath)
|
||||
if (!entry || entry.trashed) continue
|
||||
const rawContent = readUtf8File(filePath)
|
||||
if (entryMatchesSearch({ entry, rawContent, query })) results.push(searchResultFromEntry(entry))
|
||||
if (entryMatchesSearch({ entry, rawContent, query, excludeFrontmatter })) {
|
||||
results.push(searchResultFromEntry(entry))
|
||||
}
|
||||
}
|
||||
return results.slice(0, 20)
|
||||
}
|
||||
|
||||
function entryMatchesSearch({ entry, rawContent, query }: SearchEntryInput): boolean {
|
||||
return entry.title.toLowerCase().includes(query) || rawContent.toLowerCase().includes(query)
|
||||
function searchableSearchContent(rawContent: string, excludeFrontmatter: boolean): string {
|
||||
return excludeFrontmatter ? matter(rawContent).content : rawContent
|
||||
}
|
||||
|
||||
function entryMatchesSearch({ entry, excludeFrontmatter, rawContent, query }: SearchEntryInput): boolean {
|
||||
const content = searchableSearchContent(rawContent, excludeFrontmatter)
|
||||
return entry.title.toLowerCase().includes(query) || content.toLowerCase().includes(query)
|
||||
}
|
||||
|
||||
function searchResultFromEntry(entry: VaultEntry): VaultSearchResult {
|
||||
|
||||
Reference in New Issue
Block a user