Something went wrong. Try again.
A local-first note taking app
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171import { describe, it, expect } from 'vitest';
import type { FileSystemTreeNode } from '../filesystem/types';import { buildWikiLinkIndex, resolveWikiLink } from './wikiLinkIndex';
const ROOT = '/vault';
const file = (path: string): FileSystemTreeNode => ({ id: `${ROOT}/${path}`, name: path.split('/').pop() ?? path, type: 'file',});
const folder = ( path: string, children: FileSystemTreeNode[],): FileSystemTreeNode => ({ id: `${ROOT}/${path}`, name: path.split('/').pop() ?? path, type: 'folder', children,});
/** * /vault * Daily.md * logo.png * projects/ Notes.md, Deep/Notes.md * archive/ Daily.md */const TREE: FileSystemTreeNode[] = [ file('Daily.md'), file('logo.png'), folder('projects', [ file('projects/Notes.md'), folder('projects/Deep', [file('projects/Deep/Notes.md')]), ]), folder('archive', [file('archive/Daily.md')]),];
const index = buildWikiLinkIndex(ROOT, TREE);const resolveFrom = (target: string, from: string) => resolveWikiLink(index, target, `${ROOT}/${from}`);
describe('buildWikiLinkIndex', () => { it('excludes non-text files', () => { expect(resolveFrom('logo', 'Daily.md')?.exists).toBe(false); });
it('records folders for create-on-click', () => { expect(index.dirs).toContain('projects/Deep'); });
it('completes ambiguous stems as paths and unique ones as bare names', () => { const labels = index.candidates.map((c) => c.label); expect(labels).toContain('projects/Notes'); expect(labels).toContain('projects/Deep/Notes'); expect(labels).toContain('Daily'); // ambiguous too — see below });});
describe('resolveWikiLink — bare names', () => { it('matches a note anywhere in the vault', () => { const r = resolveFrom('Notes', 'Daily.md'); expect(r).toMatchObject({ exists: true, title: 'Notes.md' }); });
it('matches case-insensitively', () => { expect(resolveFrom('notes', 'Daily.md')?.exists).toBe(true); });
it('accepts an explicit `.md`', () => { expect(resolveFrom('Notes.md', 'Daily.md')?.exists).toBe(true); });
it('returns the tree node id verbatim, so tab dedup matches exactly', () => { expect(resolveFrom('Notes', 'Daily.md')?.entryId).toBe( '/vault/projects/Notes.md', ); });});
describe('resolveWikiLink — ambiguity tie-breaks', () => { it('prefers a sibling of the linking note', () => { // Two `Notes.md` exist; link from within projects/Deep. expect(resolveFrom('Notes', 'projects/Deep/Other.md')?.entryId).toBe( '/vault/projects/Deep/Notes.md', ); });
it('prefers the shallowest path when there is no sibling', () => { // Two `Daily.md` exist (root and archive/); link from projects/. expect(resolveFrom('Daily', 'projects/Notes.md')?.entryId).toBe( '/vault/Daily.md', ); });
it('prefers an exact-case match over a case-folded one', () => { const mixed = buildWikiLinkIndex(ROOT, [ folder('deep', [file('deep/note.md')]), file('Note.md'), ]); expect(resolveWikiLink(mixed, 'Note', `${ROOT}/x.md`)?.entryId).toBe( '/vault/Note.md', ); });
it('falls back to lexicographic order, not readdir order', () => { const forward = buildWikiLinkIndex(ROOT, [ folder('b', [file('b/N.md')]), folder('a', [file('a/N.md')]), ]); const reversed = buildWikiLinkIndex(ROOT, [ folder('a', [file('a/N.md')]), folder('b', [file('b/N.md')]), ]); const pick = (i: typeof forward) => resolveWikiLink(i, 'N', `${ROOT}/x.md`)?.entryId; expect(pick(forward)).toBe('/vault/a/N.md'); expect(pick(forward)).toBe(pick(reversed)); });});
describe('resolveWikiLink — path-ish targets', () => { it('resolves relative to the linking note first', () => { expect(resolveFrom('Deep/Notes', 'projects/Notes.md')?.entryId).toBe( '/vault/projects/Deep/Notes.md', ); });
it('falls back to the vault root', () => { expect(resolveFrom('projects/Notes', 'Daily.md')?.entryId).toBe( '/vault/projects/Notes.md', ); });
it('normalizes `..`', () => { expect(resolveFrom('../Daily', 'projects/Notes.md')?.entryId).toBe( '/vault/Daily.md', ); });});
describe('resolveWikiLink — misses', () => { it('would create a bare name beside the linking note', () => { expect(resolveFrom('Brand New', 'projects/Notes.md')).toEqual({ entryId: '/vault/projects/Brand New.md', title: 'Brand New.md', exists: false, }); });
it('would create a path-ish target under the vault root', () => { expect(resolveFrom('inbox/Todo', 'projects/Notes.md')).toEqual({ entryId: '/vault/inbox/Todo.md', title: 'Todo.md', exists: false, }); });
it('keeps a dotted name intact and still adds `.md`', () => { expect(resolveFrom('Release v1.2', 'Daily.md')?.entryId).toBe( '/vault/Release v1.2.md', ); });
it('returns null for an empty target', () => { expect(resolveFrom(' ', 'Daily.md')).toBeNull(); });});