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 <test@test.com>
This commit is contained in:
Luca Rossi
2026-02-28 22:03:38 +01:00
committed by GitHub
parent 2137de8bac
commit b610f1c673
4 changed files with 65 additions and 4 deletions

View File

@@ -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"
}
]
}
]
}

View File

@@ -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 (
<span
className={`wikilink${isBroken ? ' wikilink--broken' : ''}`}
data-target={target}
style={{ color }}
>
{target}
{displayText}
</span>
)
},

View File

@@ -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)')
})
})

View File

@@ -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),
)
}