fix: preserve source-relative Tolaria page routes
This commit is contained in:
parent
2457821eeb
commit
d7afc7943d
@ -45,6 +45,9 @@
|
|||||||
- 增强引用提示面板、段落节奏、代码块、表格与行内代码层次。
|
- 增强引用提示面板、段落节奏、代码块、表格与行内代码层次。
|
||||||
9. `0009-test-lock-in-readable-editor-theme-defaults.patch`
|
9. `0009-test-lock-in-readable-editor-theme-defaults.patch`
|
||||||
- 固化更宽的阅读列与带颜色的行内代码主题默认值。
|
- 固化更宽的阅读列与带颜色的行内代码主题默认值。
|
||||||
|
10. `0010-fix-resolve-relative-page-links-from-source-notes.patch`
|
||||||
|
- 按当前笔记所在目录归一化 `../` 与 `./` 页面路径;
|
||||||
|
- 避免多个页面都叫 `README.md` 时跳到错误页面。
|
||||||
|
|
||||||
## 恢复与验证
|
## 恢复与验证
|
||||||
|
|
||||||
@ -58,12 +61,13 @@ git am /path/to/0006-fix-make-Guanghu-home-and-themes-visible.patch
|
|||||||
git am /path/to/0007-feat-complete-portable-page-block-set.patch
|
git am /path/to/0007-feat-complete-portable-page-block-set.patch
|
||||||
git am /path/to/0008-fix-restore-internal-note-links-and-reading-rhythm.patch
|
git am /path/to/0008-fix-restore-internal-note-links-and-reading-rhythm.patch
|
||||||
git am /path/to/0009-test-lock-in-readable-editor-theme-defaults.patch
|
git am /path/to/0009-test-lock-in-readable-editor-theme-defaults.patch
|
||||||
|
git am /path/to/0010-fix-resolve-relative-page-links-from-source-notes.patch
|
||||||
pnpm build
|
pnpm build
|
||||||
pnpm lint
|
pnpm lint
|
||||||
```
|
```
|
||||||
|
|
||||||
`0001`、`0002` 保留为早期两补丁拆分档案;只有在不使用 `0003`
|
`0001`、`0002` 保留为早期两补丁拆分档案;只有在不使用 `0003`
|
||||||
原型系列时才单独应用。上述 `0003 → 0009` 顺序已于 2026-07-17
|
原型系列时才单独应用。上述 `0003 → 0010` 顺序已于 2026-07-17
|
||||||
在服务器最新 `bingshuo/guanghu@514ab19` 的新克隆上完整复现。
|
在服务器最新 `bingshuo/guanghu@514ab19` 的新克隆上完整复现。
|
||||||
|
|
||||||
## 研发规则
|
## 研发规则
|
||||||
|
|||||||
@ -0,0 +1,176 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
@ -30,6 +30,7 @@ Tolaria 页面路由的兼容层;当前页面还把部分参考路径放在代
|
|||||||
|
|
||||||
- `apps/tolaria/patches/0008-fix-restore-internal-note-links-and-reading-rhythm.patch`
|
- `apps/tolaria/patches/0008-fix-restore-internal-note-links-and-reading-rhythm.patch`
|
||||||
- `apps/tolaria/patches/0009-test-lock-in-readable-editor-theme-defaults.patch`
|
- `apps/tolaria/patches/0009-test-lock-in-readable-editor-theme-defaults.patch`
|
||||||
|
- `apps/tolaria/patches/0010-fix-resolve-relative-page-links-from-source-notes.patch`
|
||||||
- 桌面验收包:`HoloLake Era 0.1.4 Review`
|
- 桌面验收包:`HoloLake Era 0.1.4 Review`
|
||||||
- Bundle ID:`com.guanghu.desktop.review.20260717.page`
|
- Bundle ID:`com.guanghu.desktop.review.20260717.page`
|
||||||
- DMG SHA-256:`846884e8…c66bc303a`
|
- DMG SHA-256:`846884e8…c66bc303a`
|
||||||
@ -37,6 +38,7 @@ Tolaria 页面路由的兼容层;当前页面还把部分参考路径放在代
|
|||||||
## 验证
|
## 验证
|
||||||
|
|
||||||
- 回归测试先失败后修复:相对 Markdown 页面链接普通单击跳转;
|
- 回归测试先失败后修复:相对 Markdown 页面链接普通单击跳转;
|
||||||
|
- 原生点击发现重复 `README.md` 会误跳后,再补回归测试:路径按来源页面目录归一化并跳到唯一正确页面;
|
||||||
- 定向 Vitest:16 项通过;
|
- 定向 Vitest:16 项通过;
|
||||||
- ESLint、TypeScript 与 Vite production build 通过;
|
- ESLint、TypeScript 与 Vite production build 通过;
|
||||||
- Tauri Apple Silicon App 与 DMG 构建通过;
|
- Tauri Apple Silicon App 与 DMG 构建通过;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user