// Search for the stack picker: every typed word must start a word in a note's title or tags. import { byTitle } from './stack.mjs'; /** Lowercase words without accents; periods and apostrophes join, so "A.I." is "ai". @type {(text: string) => string[]} */ const words = (text) => text .normalize('NFD').replace(/\p{M}/gu, '') .toLowerCase() .replace(/[.'’]/g, '') .split(/[^\p{L}\p{N}]+/u) .filter(Boolean); /** Whether some word in `list` starts with `w`. @type {(list: string[], w: string) => boolean} */ const starts = (list, w) => list.some((t) => t.startsWith(w)); /** * How `entry` matches the query words: `null`, `{ title: true, leads }`, or `{ tag }` for a tag-only match. * @param {{ title: string, tags: string[] }} entry * @param {string[]} q */ function match(entry, q) { const title = words(entry.title); let tag; for (const w of q) { if (starts(title, w)) continue; const t = entry.tags.find((x) => starts(words(x), w)); if (t == null) return null; tag ??= t; } if (tag != null) return { tag }; // match-sorter's tiers: a title that starts with the whole query beats one with matching words. return { title: true, leads: title.join(' ').startsWith(q.join(' ')) }; } /** * Notes matching `query`, as `[{ entry, tag? }]` in display order: titles that start with the * query, other title matches, then notes where some word only matched a tag (that tag is `tag`), * each alphabetical. An empty query lists every note. * @template {{ title: string, tags: string[] }} T * @param {T[]} entries * @param {string} query * @returns {{ entry: T, tag?: string }[]} */ export function searchNotes(entries, query) { const q = words(query); /** @type {{ entry: T, tag?: string }[][]} */ const tiers = [[], [], []]; for (const entry of [...entries].sort(byTitle)) { const m = match(entry, q); if (!m) continue; if (m.title) tiers[m.leads ? 0 : 1].push({ entry }); else tiers[2].push({ entry, tag: m.tag }); } return tiers.flat(); }