fix: resolve relative page links from source notes
This commit is contained in:
parent
59ce6261d3
commit
63a544d9fb
@ -1208,7 +1208,7 @@ export function SingleEditorView({ editor, entries, onNavigateWikilink, onChange
|
|||||||
handleMouseMove: handleCodeBlockCopyMouseMove,
|
handleMouseMove: handleCodeBlockCopyMouseMove,
|
||||||
} = useCodeBlockCopyTarget(containerRef)
|
} = useCodeBlockCopyTarget(containerRef)
|
||||||
useBlockNoteSideMenuHoverGuard(containerRef)
|
useBlockNoteSideMenuHoverGuard(containerRef)
|
||||||
useEditorLinkActivation(containerRef, onNavigateWikilink, vaultPath)
|
useEditorLinkActivation(containerRef, onNavigateWikilink, vaultPath, sourceEntry?.path)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
_wikilinkEntriesRef.current = entries
|
_wikilinkEntriesRef.current = entries
|
||||||
|
|||||||
@ -20,17 +20,19 @@ const mockOpenLocalFile = vi.mocked(openLocalFile)
|
|||||||
function Harness({
|
function Harness({
|
||||||
onNavigateWikilink,
|
onNavigateWikilink,
|
||||||
vaultPath,
|
vaultPath,
|
||||||
|
sourcePath,
|
||||||
}: {
|
}: {
|
||||||
onNavigateWikilink: (target: string) => void
|
onNavigateWikilink: (target: string) => void
|
||||||
vaultPath?: string
|
vaultPath?: string
|
||||||
|
sourcePath?: string
|
||||||
}) {
|
}) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
useEditorLinkActivation(containerRef, onNavigateWikilink, vaultPath)
|
useEditorLinkActivation(containerRef, onNavigateWikilink, vaultPath, sourcePath)
|
||||||
return <div ref={containerRef} data-testid="editor-link-container" />
|
return <div ref={containerRef} data-testid="editor-link-container" />
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderHarness(onNavigateWikilink = vi.fn(), vaultPath?: string) {
|
function renderHarness(onNavigateWikilink = vi.fn(), vaultPath?: string, sourcePath?: string) {
|
||||||
render(<Harness onNavigateWikilink={onNavigateWikilink} vaultPath={vaultPath} />)
|
render(<Harness onNavigateWikilink={onNavigateWikilink} vaultPath={vaultPath} sourcePath={sourcePath} />)
|
||||||
return {
|
return {
|
||||||
container: screen.getByTestId('editor-link-container') as HTMLDivElement,
|
container: screen.getByTestId('editor-link-container') as HTMLDivElement,
|
||||||
onNavigateWikilink,
|
onNavigateWikilink,
|
||||||
@ -129,7 +131,7 @@ describe('useEditorLinkActivation', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('opens relative Markdown note links inside Tolaria on a plain click', async () => {
|
it('opens relative Markdown note links inside Tolaria on a plain click', async () => {
|
||||||
const { container, onNavigateWikilink } = renderHarness()
|
const { container, onNavigateWikilink } = renderHarness(vi.fn(), '/vault', '/vault/docs/NEXT.md')
|
||||||
const link = appendUrl(container, '../Migration/Related%20Page%20abc123.md#current-status')
|
const link = appendUrl(container, '../Migration/Related%20Page%20abc123.md#current-status')
|
||||||
|
|
||||||
const plainClick = dispatchMouseEvent(link, 'click')
|
const plainClick = dispatchMouseEvent(link, 'click')
|
||||||
@ -137,7 +139,17 @@ describe('useEditorLinkActivation', () => {
|
|||||||
expect(plainClick.defaultPrevented).toBe(true)
|
expect(plainClick.defaultPrevented).toBe(true)
|
||||||
expect(mockOpenLocalFile).not.toHaveBeenCalled()
|
expect(mockOpenLocalFile).not.toHaveBeenCalled()
|
||||||
await Promise.resolve()
|
await Promise.resolve()
|
||||||
expect(onNavigateWikilink).toHaveBeenCalledWith('../Migration/Related Page abc123')
|
expect(onNavigateWikilink).toHaveBeenCalledWith('Migration/Related Page abc123')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resolves duplicate README links from the current note directory', async () => {
|
||||||
|
const { container, onNavigateWikilink } = renderHarness(vi.fn(), '/vault', '/vault/docs/NEXT.md')
|
||||||
|
const link = appendUrl(container, '../apps/tolaria/modules/02-knowledge-rendering/README.md')
|
||||||
|
|
||||||
|
dispatchMouseEvent(link, 'click')
|
||||||
|
|
||||||
|
await Promise.resolve()
|
||||||
|
expect(onNavigateWikilink).toHaveBeenCalledWith('apps/tolaria/modules/02-knowledge-rendering/README')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('consumes Markdown note link mousedown before editor internals can select it', async () => {
|
it('consumes Markdown note link mousedown before editor internals can select it', async () => {
|
||||||
|
|||||||
@ -34,14 +34,46 @@ function decodeLinkPath(path: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveMarkdownNoteTarget(href: string) {
|
function normalizePathSegments(path: string) {
|
||||||
|
const segments: string[] = []
|
||||||
|
for (const segment of path.split('/')) {
|
||||||
|
if (!segment || segment === '.') continue
|
||||||
|
if (segment === '..') {
|
||||||
|
segments.pop()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
segments.push(segment)
|
||||||
|
}
|
||||||
|
return segments.join('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceRelativeDirectory(vaultPath?: string, sourcePath?: string) {
|
||||||
|
if (!vaultPath || !sourcePath) return null
|
||||||
|
|
||||||
|
const normalizedVault = vaultPath.replace(/\\/gu, '/').replace(/\/+$/u, '')
|
||||||
|
const normalizedSource = sourcePath.replace(/\\/gu, '/')
|
||||||
|
const prefix = `${normalizedVault}/`
|
||||||
|
if (!normalizedSource.toLowerCase().startsWith(prefix.toLowerCase())) return null
|
||||||
|
|
||||||
|
const relativeSource = normalizedSource.slice(prefix.length)
|
||||||
|
const slashIndex = relativeSource.lastIndexOf('/')
|
||||||
|
return slashIndex === -1 ? '' : relativeSource.slice(0, slashIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMarkdownNoteTarget(href: string, vaultPath?: string, sourcePath?: string) {
|
||||||
const path = href.split(/[?#]/u, 1)[0]?.trim() ?? ''
|
const path = href.split(/[?#]/u, 1)[0]?.trim() ?? ''
|
||||||
if (!path || path.startsWith('//') || /^[a-z][a-z\d+.-]*:/iu.test(path)) return null
|
if (!path || path.startsWith('//') || /^[a-z][a-z\d+.-]*:/iu.test(path)) return null
|
||||||
|
|
||||||
const decodedPath = decodeLinkPath(path).replace(/\\/gu, '/')
|
const decodedPath = decodeLinkPath(path).replace(/\\/gu, '/')
|
||||||
if (!/\.md(?:own)?$/iu.test(decodedPath)) return null
|
if (!/\.md(?:own)?$/iu.test(decodedPath)) return null
|
||||||
|
|
||||||
return decodedPath.replace(/\.md(?:own)?$/iu, '')
|
const pathWithoutExtension = decodedPath.replace(/\.md(?:own)?$/iu, '')
|
||||||
|
if (pathWithoutExtension.startsWith('/')) return normalizePathSegments(pathWithoutExtension)
|
||||||
|
|
||||||
|
const sourceDirectory = sourceRelativeDirectory(vaultPath, sourcePath)
|
||||||
|
return normalizePathSegments(
|
||||||
|
sourceDirectory === null ? pathWithoutExtension : `${sourceDirectory}/${pathWithoutExtension}`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function blurActiveEditable(container: HTMLElement) {
|
function blurActiveEditable(container: HTMLElement) {
|
||||||
@ -91,6 +123,7 @@ function handleEditorLinkClick(
|
|||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
onNavigateWikilink: (target: string) => void,
|
onNavigateWikilink: (target: string) => void,
|
||||||
vaultPath?: string,
|
vaultPath?: string,
|
||||||
|
sourcePath?: string,
|
||||||
) {
|
) {
|
||||||
const target = elementFromEventTarget(event.target)
|
const target = elementFromEventTarget(event.target)
|
||||||
if (!target || isInsideCodeContext(target)) return
|
if (!target || isInsideCodeContext(target)) return
|
||||||
@ -104,7 +137,7 @@ function handleEditorLinkClick(
|
|||||||
const href = resolveAnchorHref(target)
|
const href = resolveAnchorHref(target)
|
||||||
if (!href) return
|
if (!href) return
|
||||||
|
|
||||||
const markdownTarget = resolveMarkdownNoteTarget(href)
|
const markdownTarget = resolveMarkdownNoteTarget(href, vaultPath, sourcePath)
|
||||||
if (markdownTarget) {
|
if (markdownTarget) {
|
||||||
activateWikilink(event, container, markdownTarget, onNavigateWikilink)
|
activateWikilink(event, container, markdownTarget, onNavigateWikilink)
|
||||||
return
|
return
|
||||||
@ -146,6 +179,7 @@ export function useEditorLinkActivation(
|
|||||||
containerRef: RefObject<HTMLDivElement | null>,
|
containerRef: RefObject<HTMLDivElement | null>,
|
||||||
onNavigateWikilink: (target: string) => void,
|
onNavigateWikilink: (target: string) => void,
|
||||||
vaultPath?: string,
|
vaultPath?: string,
|
||||||
|
sourcePath?: string,
|
||||||
) {
|
) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current
|
const container = containerRef.current
|
||||||
@ -187,7 +221,7 @@ export function useEditorLinkActivation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearHandledMouseDownUrl()
|
clearHandledMouseDownUrl()
|
||||||
handleEditorLinkClick(event, container, onNavigateWikilink, vaultPath)
|
handleEditorLinkClick(event, container, onNavigateWikilink, vaultPath, sourcePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
container.addEventListener('mousedown', handleMouseDown, true)
|
container.addEventListener('mousedown', handleMouseDown, true)
|
||||||
@ -207,5 +241,5 @@ export function useEditorLinkActivation(
|
|||||||
clearHandledMouseDownUrl()
|
clearHandledMouseDownUrl()
|
||||||
resetModifierState()
|
resetModifierState()
|
||||||
}
|
}
|
||||||
}, [containerRef, onNavigateWikilink, vaultPath])
|
}, [containerRef, onNavigateWikilink, sourcePath, vaultPath])
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user