Some checks are pending
Build Windows Internal Installer / Build Windows x64 internal installer (push) Waiting to run
Source snapshot: 789bd691f530f915fc870bf72ecb7c96c5d5101c
177 lines
7.4 KiB
Diff
177 lines
7.4 KiB
Diff
From 63a544d9fbb30bdcdb7377947fe22947a5276005 Mon Sep 17 00:00:00 2001
|
|
From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com>
|
|
Date: Fri, 17 Jul 2026 11:26:04 +0800
|
|
Subject: [PATCH] fix: resolve relative page links from source notes
|
|
|
|
---
|
|
src/components/SingleEditorView.tsx | 2 +-
|
|
.../useEditorLinkActivation.test.tsx | 22 +++++++---
|
|
src/components/useEditorLinkActivation.ts | 44 ++++++++++++++++---
|
|
3 files changed, 57 insertions(+), 11 deletions(-)
|
|
|
|
diff --git a/src/components/SingleEditorView.tsx b/src/components/SingleEditorView.tsx
|
|
index df43bd1..0440e5a 100644
|
|
--- a/src/components/SingleEditorView.tsx
|
|
+++ b/src/components/SingleEditorView.tsx
|
|
@@ -1208,7 +1208,7 @@ export function SingleEditorView({ editor, entries, onNavigateWikilink, onChange
|
|
handleMouseMove: handleCodeBlockCopyMouseMove,
|
|
} = useCodeBlockCopyTarget(containerRef)
|
|
useBlockNoteSideMenuHoverGuard(containerRef)
|
|
- useEditorLinkActivation(containerRef, onNavigateWikilink, vaultPath)
|
|
+ useEditorLinkActivation(containerRef, onNavigateWikilink, vaultPath, sourceEntry?.path)
|
|
|
|
useEffect(() => {
|
|
_wikilinkEntriesRef.current = entries
|
|
diff --git a/src/components/useEditorLinkActivation.test.tsx b/src/components/useEditorLinkActivation.test.tsx
|
|
index bbfcaee..321bc00 100644
|
|
--- a/src/components/useEditorLinkActivation.test.tsx
|
|
+++ b/src/components/useEditorLinkActivation.test.tsx
|
|
@@ -20,17 +20,19 @@ const mockOpenLocalFile = vi.mocked(openLocalFile)
|
|
function Harness({
|
|
onNavigateWikilink,
|
|
vaultPath,
|
|
+ sourcePath,
|
|
}: {
|
|
onNavigateWikilink: (target: string) => void
|
|
vaultPath?: string
|
|
+ sourcePath?: string
|
|
}) {
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
- useEditorLinkActivation(containerRef, onNavigateWikilink, vaultPath)
|
|
+ useEditorLinkActivation(containerRef, onNavigateWikilink, vaultPath, sourcePath)
|
|
return <div ref={containerRef} data-testid="editor-link-container" />
|
|
}
|
|
|
|
-function renderHarness(onNavigateWikilink = vi.fn(), vaultPath?: string) {
|
|
- render(<Harness onNavigateWikilink={onNavigateWikilink} vaultPath={vaultPath} />)
|
|
+function renderHarness(onNavigateWikilink = vi.fn(), vaultPath?: string, sourcePath?: string) {
|
|
+ render(<Harness onNavigateWikilink={onNavigateWikilink} vaultPath={vaultPath} sourcePath={sourcePath} />)
|
|
return {
|
|
container: screen.getByTestId('editor-link-container') as HTMLDivElement,
|
|
onNavigateWikilink,
|
|
@@ -129,7 +131,7 @@ describe('useEditorLinkActivation', () => {
|
|
})
|
|
|
|
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 plainClick = dispatchMouseEvent(link, 'click')
|
|
@@ -137,7 +139,17 @@ describe('useEditorLinkActivation', () => {
|
|
expect(plainClick.defaultPrevented).toBe(true)
|
|
expect(mockOpenLocalFile).not.toHaveBeenCalled()
|
|
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 () => {
|
|
diff --git a/src/components/useEditorLinkActivation.ts b/src/components/useEditorLinkActivation.ts
|
|
index 79a2dad..31c49e0 100644
|
|
--- a/src/components/useEditorLinkActivation.ts
|
|
+++ b/src/components/useEditorLinkActivation.ts
|
|
@@ -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() ?? ''
|
|
if (!path || path.startsWith('//') || /^[a-z][a-z\d+.-]*:/iu.test(path)) return null
|
|
|
|
const decodedPath = decodeLinkPath(path).replace(/\\/gu, '/')
|
|
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) {
|
|
@@ -91,6 +123,7 @@ function handleEditorLinkClick(
|
|
container: HTMLElement,
|
|
onNavigateWikilink: (target: string) => void,
|
|
vaultPath?: string,
|
|
+ sourcePath?: string,
|
|
) {
|
|
const target = elementFromEventTarget(event.target)
|
|
if (!target || isInsideCodeContext(target)) return
|
|
@@ -104,7 +137,7 @@ function handleEditorLinkClick(
|
|
const href = resolveAnchorHref(target)
|
|
if (!href) return
|
|
|
|
- const markdownTarget = resolveMarkdownNoteTarget(href)
|
|
+ const markdownTarget = resolveMarkdownNoteTarget(href, vaultPath, sourcePath)
|
|
if (markdownTarget) {
|
|
activateWikilink(event, container, markdownTarget, onNavigateWikilink)
|
|
return
|
|
@@ -146,6 +179,7 @@ export function useEditorLinkActivation(
|
|
containerRef: RefObject<HTMLDivElement | null>,
|
|
onNavigateWikilink: (target: string) => void,
|
|
vaultPath?: string,
|
|
+ sourcePath?: string,
|
|
) {
|
|
useEffect(() => {
|
|
const container = containerRef.current
|
|
@@ -187,7 +221,7 @@ export function useEditorLinkActivation(
|
|
}
|
|
|
|
clearHandledMouseDownUrl()
|
|
- handleEditorLinkClick(event, container, onNavigateWikilink, vaultPath)
|
|
+ handleEditorLinkClick(event, container, onNavigateWikilink, vaultPath, sourcePath)
|
|
}
|
|
|
|
container.addEventListener('mousedown', handleMouseDown, true)
|
|
@@ -207,5 +241,5 @@ export function useEditorLinkActivation(
|
|
clearHandledMouseDownUrl()
|
|
resetModifierState()
|
|
}
|
|
- }, [containerRef, onNavigateWikilink, vaultPath])
|
|
+ }, [containerRef, onNavigateWikilink, sourcePath, vaultPath])
|
|
}
|
|
--
|
|
2.50.1 (Apple Git-155)
|
|
|