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:
17
design/gs-wikilinks-fix.pen
Normal file
17
design/gs-wikilinks-fix.pen
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -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)')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user