From b610f1c673196892571e469fc8c7023eb49823e2 Mon Sep 17 00:00:00 2001 From: Luca Rossi Date: Sat, 28 Feb 2026 22:03:38 +0100 Subject: [PATCH] fix: resolve wikilink paths and show entry title in Getting Started vault (#153) * fix: resolve wikilink paths and show entry title in Getting Started vault - Add path suffix matching in findEntryByTarget() so path-based targets like 'note/welcome-to-laputa' match entries at /note/welcome-to-laputa.md - Add resolveDisplayText() in editorSchema to show entry title instead of raw path - Priority: pipe display text > entry title > humanized path stem - 6 new test cases covering path-based matching and color resolution * style: fix rustfmt formatting in mcp.rs --------- Co-authored-by: Test --- design/gs-wikilinks-fix.pen | 17 +++++++++++++++++ src/components/editorSchema.tsx | 16 ++++++++++++++-- src/utils/wikilinkColors.test.ts | 32 +++++++++++++++++++++++++++++++- src/utils/wikilinkColors.ts | 4 +++- 4 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 design/gs-wikilinks-fix.pen diff --git a/design/gs-wikilinks-fix.pen b/design/gs-wikilinks-fix.pen new file mode 100644 index 00000000..448ebce2 --- /dev/null +++ b/design/gs-wikilinks-fix.pen @@ -0,0 +1,17 @@ +{ + "children": [ + { + "name": "Getting Started Wikilinks — Fixed (styled with type color)", + "type": "FRAME", + "width": 800, + "height": 400, + "children": [ + { + "name": "Description", + "type": "TEXT", + "characters": "Wikilinks in Getting Started vault now resolve correctly:\n- path/to/note targets match entry paths ending in /path/to/note.md\n- Display text shows entry title (not raw path)\n- Color reflects the linked note's type (e.g. blue for Note type)\n- Broken links show muted color as expected\n\nBefore: [[note/welcome-to-laputa]] showed raw path, unstyled\nAfter: [[note/welcome-to-laputa]] shows 'Welcome to Laputa' in Note blue" + } + ] + } + ] +} diff --git a/src/components/editorSchema.tsx b/src/components/editorSchema.tsx index b8bd9d4d..2a5d545f 100644 --- a/src/components/editorSchema.tsx +++ b/src/components/editorSchema.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-refresh/only-export-components -- module-level schema, not a component file */ import { BlockNoteSchema, defaultInlineContentSpecs } from '@blocknote/core' import { createReactInlineContentSpec } from '@blocknote/react' -import { resolveWikilinkColor as resolveColor } from '../utils/wikilinkColors' +import { resolveWikilinkColor as resolveColor, findEntryByTarget } from '../utils/wikilinkColors' import type { VaultEntry } from '../types' // Module-level cache so the WikiLink renderer (defined outside React) can access entries @@ -11,6 +11,17 @@ function resolveWikilinkColor(target: string) { return resolveColor(_wikilinkEntriesRef.current, target) } +/** Resolve the display text for a wikilink target. + * Priority: pipe display text → entry title → humanised path stem */ +function resolveDisplayText(target: string): string { + const pipeIdx = target.indexOf('|') + if (pipeIdx !== -1) return target.slice(pipeIdx + 1) + const entry = findEntryByTarget(_wikilinkEntriesRef.current, target) + if (entry) return entry.title + const last = target.split('/').pop() ?? target + return last.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) +} + export const WikiLink = createReactInlineContentSpec( { type: "wikilink" as const, @@ -23,13 +34,14 @@ export const WikiLink = createReactInlineContentSpec( render: (props) => { const target = props.inlineContent.props.target const { color, isBroken } = resolveWikilinkColor(target) + const displayText = resolveDisplayText(target) return ( - {target} + {displayText} ) }, diff --git a/src/utils/wikilinkColors.test.ts b/src/utils/wikilinkColors.test.ts index 9ca959e5..25872284 100644 --- a/src/utils/wikilinkColors.test.ts +++ b/src/utils/wikilinkColors.test.ts @@ -36,6 +36,7 @@ const typePerson = makeEntry({ path: '/vault/type/person.md', filename: 'person. const typeEvent = makeEntry({ path: '/vault/type/event.md', filename: 'event.md', title: 'Event', isA: 'Type', color: 'yellow' }) const typeTopic = makeEntry({ path: '/vault/type/topic.md', filename: 'topic.md', title: 'Topic', isA: 'Type', color: 'green' }) const typeRecipe = makeEntry({ path: '/vault/type/recipe.md', filename: 'recipe.md', title: 'Recipe', isA: 'Type', color: 'orange', icon: 'cooking-pot' }) +const typeNote = makeEntry({ path: '/vault/type/note.md', filename: 'note.md', title: 'Note', isA: 'Type', color: 'blue' }) const projectEntry = makeEntry({ path: '/vault/project/app.md', filename: 'app.md', title: 'Build App', isA: 'Project' }) const personEntry = makeEntry({ path: '/vault/person/alice.md', filename: 'alice.md', title: 'Alice', isA: 'Person', aliases: ['Alice Smith'] }) @@ -43,8 +44,9 @@ const eventEntry = makeEntry({ path: '/vault/event/kickoff.md', filename: 'kicko const topicEntry = makeEntry({ path: '/vault/topic/dev.md', filename: 'dev.md', title: 'Software Development', isA: 'Topic' }) const recipeEntry = makeEntry({ path: '/vault/recipe/pasta.md', filename: 'pasta.md', title: 'Pasta Carbonara', isA: 'Recipe' }) const untypedEntry = makeEntry({ path: '/vault/note/random.md', filename: 'random.md', title: 'Random Thought' }) +const noteEntry = makeEntry({ path: '/vault/note/welcome-to-laputa.md', filename: 'welcome-to-laputa.md', title: 'Welcome to Laputa', isA: 'Note' }) -const allEntries = [typeProject, typePerson, typeEvent, typeTopic, typeRecipe, projectEntry, personEntry, eventEntry, topicEntry, recipeEntry, untypedEntry] +const allEntries = [typeProject, typePerson, typeEvent, typeTopic, typeRecipe, typeNote, projectEntry, personEntry, eventEntry, topicEntry, recipeEntry, untypedEntry, noteEntry] describe('findEntryByTarget', () => { it('matches by title', () => { @@ -66,6 +68,22 @@ describe('findEntryByTarget', () => { it('returns undefined for non-existent target', () => { expect(findEntryByTarget(allEntries, 'Non Existent')).toBeUndefined() }) + + it('matches by relative path (folder/slug)', () => { + expect(findEntryByTarget(allEntries, 'note/welcome-to-laputa')).toBe(noteEntry) + }) + + it('matches by relative path with pipe syntax', () => { + expect(findEntryByTarget(allEntries, 'note/welcome-to-laputa|Welcome!')).toBe(noteEntry) + }) + + it('matches project by relative path', () => { + expect(findEntryByTarget(allEntries, 'project/app')).toBe(projectEntry) + }) + + it('matches person by relative path', () => { + expect(findEntryByTarget(allEntries, 'person/alice')).toBe(personEntry) + }) }) describe('lookupColorForEntry', () => { @@ -130,4 +148,16 @@ describe('resolveWikilinkColor', () => { expect(result.isBroken).toBe(false) expect(result.color).toBe('var(--accent-yellow)') }) + + it('resolves relative-path wikilink target to correct type color', () => { + const result = resolveWikilinkColor(allEntries, 'note/welcome-to-laputa') + expect(result.isBroken).toBe(false) + expect(result.color).toBe('var(--accent-blue)') + }) + + it('resolves relative-path with pipe syntax to correct type color', () => { + const result = resolveWikilinkColor(allEntries, 'person/alice|Alice S.') + expect(result.isBroken).toBe(false) + expect(result.color).toBe('var(--accent-yellow)') + }) }) diff --git a/src/utils/wikilinkColors.ts b/src/utils/wikilinkColors.ts index a2823fd2..fcc94b65 100644 --- a/src/utils/wikilinkColors.ts +++ b/src/utils/wikilinkColors.ts @@ -12,10 +12,12 @@ const BROKEN_LINK_COLOR = 'var(--text-muted)' export function findEntryByTarget(entries: VaultEntry[], target: string): VaultEntry | undefined { // Handle pipe syntax: [[path|display name]] → use path part for matching const key = target.includes('|') ? target.split('|')[0] : target + const suffix = '/' + key + '.md' return entries.find(e => e.title === key || e.filename.replace(/\.md$/, '') === key || - e.aliases.includes(key), + e.aliases.includes(key) || + e.path.endsWith(suffix), ) }