feat: add regex mode for view filters
This commit is contained in:
@@ -352,4 +352,45 @@ describe('evaluateView', () => {
|
||||
const result = evaluateView(view, entries)
|
||||
expect(result.map((e) => e.title)).toEqual(['Empty'])
|
||||
})
|
||||
|
||||
it('supports regex matching on scalar fields', () => {
|
||||
const view: ViewDefinition = {
|
||||
name: 'Regex title', icon: null, color: null, sort: null,
|
||||
filters: { all: [{ field: 'title', op: 'contains', value: '^alpha\\s+project$', regex: true }] },
|
||||
}
|
||||
const entries = [
|
||||
makeEntry({ title: 'Alpha Project' }),
|
||||
makeEntry({ title: 'Alpha Notes' }),
|
||||
makeEntry({ title: 'alpha project' }),
|
||||
]
|
||||
const result = evaluateView(view, entries)
|
||||
expect(result.map((e) => e.title)).toEqual(['Alpha Project', 'alpha project'])
|
||||
})
|
||||
|
||||
it('supports regex matching on relationship aliases and stems', () => {
|
||||
const view: ViewDefinition = {
|
||||
name: 'Regex relationship', icon: null, color: null, sort: null,
|
||||
filters: { all: [{ field: 'belongs to', op: 'contains', value: 'monday-(112|113)|Monday #112', regex: true }] },
|
||||
}
|
||||
const entries = [
|
||||
makeEntry({ title: 'Alias match', relationships: { 'belongs to': ['[[monday-112|Monday #112]]'] } }),
|
||||
makeEntry({ title: 'Stem match', relationships: { 'belongs to': ['[[monday-113]]'] } }),
|
||||
makeEntry({ title: 'No match', relationships: { 'belongs to': ['[[tuesday-200|Tuesday]]'] } }),
|
||||
]
|
||||
const result = evaluateView(view, entries)
|
||||
expect(result.map((e) => e.title)).toEqual(['Alias match', 'Stem match'])
|
||||
})
|
||||
|
||||
it('treats invalid regex filters as matching nothing', () => {
|
||||
const view: ViewDefinition = {
|
||||
name: 'Broken regex', icon: null, color: null, sort: null,
|
||||
filters: { all: [{ field: 'title', op: 'contains', value: '(', regex: true }] },
|
||||
}
|
||||
const entries = [
|
||||
makeEntry({ title: 'Alpha Project' }),
|
||||
makeEntry({ title: 'Beta Project' }),
|
||||
]
|
||||
const result = evaluateView(view, entries)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -50,6 +50,18 @@ function wikilinkStem(raw: string): string {
|
||||
return s.toLowerCase()
|
||||
}
|
||||
|
||||
function relationshipCandidates(raw: string): string[] {
|
||||
const trimmed = raw.trim()
|
||||
let inner = trimmed
|
||||
if (inner.startsWith('[[')) inner = inner.slice(2)
|
||||
if (inner.endsWith(']]')) inner = inner.slice(0, -2)
|
||||
const pipe = inner.indexOf('|')
|
||||
if (pipe >= 0) {
|
||||
return [trimmed, inner.slice(0, pipe), inner.slice(pipe + 1)]
|
||||
}
|
||||
return [trimmed, inner]
|
||||
}
|
||||
|
||||
/** Extract all comparable parts (path and alias) from a wikilink string. */
|
||||
function wikilinkParts(raw: string): string[] {
|
||||
let s = raw.trim()
|
||||
@@ -73,6 +85,20 @@ function toString(v: unknown): string {
|
||||
return String(v)
|
||||
}
|
||||
|
||||
function compileRegex(cond: FilterCondition, value: string): RegExp | null {
|
||||
if (cond.regex !== true) return null
|
||||
try {
|
||||
return new RegExp(value, 'i')
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function usesRegex(cond: FilterCondition): boolean {
|
||||
return cond.regex === true
|
||||
&& (cond.op === 'contains' || cond.op === 'not_contains' || cond.op === 'equals' || cond.op === 'not_equals')
|
||||
}
|
||||
|
||||
function evaluateCondition(cond: FilterCondition, entry: VaultEntry): boolean {
|
||||
const resolved = resolveField(entry, cond.field)
|
||||
const { op, value } = cond
|
||||
@@ -89,8 +115,17 @@ function evaluateCondition(cond: FilterCondition, entry: VaultEntry): boolean {
|
||||
}
|
||||
|
||||
const condVal = toString(value)
|
||||
const regex = usesRegex(cond) ? compileRegex(cond, condVal) : null
|
||||
|
||||
if (usesRegex(cond) && !regex) return false
|
||||
|
||||
if (resolved.array) {
|
||||
if (regex) {
|
||||
const matched = resolved.array.some((item) => relationshipCandidates(item).some((candidate) => regex.test(candidate)))
|
||||
if (op === 'contains' || op === 'equals') return matched
|
||||
if (op === 'not_contains' || op === 'not_equals') return !matched
|
||||
}
|
||||
|
||||
const stem = wikilinkStem(condVal)
|
||||
const isWikilink = condVal.trim().startsWith('[[')
|
||||
const arrayMatch = (arr: string[]) => arr.some((item) =>
|
||||
@@ -111,7 +146,14 @@ function evaluateCondition(cond: FilterCondition, entry: VaultEntry): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
const fieldStr = toString(resolved.scalar).toLowerCase()
|
||||
const fieldRaw = toString(resolved.scalar)
|
||||
if (regex) {
|
||||
const matched = regex.test(fieldRaw)
|
||||
if (op === 'equals' || op === 'contains') return matched
|
||||
if (op === 'not_equals' || op === 'not_contains') return !matched
|
||||
}
|
||||
|
||||
const fieldStr = fieldRaw.toLowerCase()
|
||||
const condStr = condVal.toLowerCase()
|
||||
|
||||
if (op === 'equals') return fieldStr === condStr
|
||||
|
||||
Reference in New Issue
Block a user