hololake-platform/apps/tolaria/patches/0010-fix-resolve-relative-page-links-from-source-notes.patch

177 lines
7.4 KiB
Diff
Raw Normal View History

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)