From d7afc7943d90f2157ec10ba7913d9b348b2b453f 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:48 +0800 Subject: [PATCH] fix: preserve source-relative Tolaria page routes --- apps/tolaria/README.md | 6 +- ...elative-page-links-from-source-notes.patch | 176 ++++++++++++++++++ ...OTION-READING-AND-PAGE-LINK-RESTORATION.md | 2 + 3 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 apps/tolaria/patches/0010-fix-resolve-relative-page-links-from-source-notes.patch diff --git a/apps/tolaria/README.md b/apps/tolaria/README.md index 1dedf18..f2260be 100644 --- a/apps/tolaria/README.md +++ b/apps/tolaria/README.md @@ -45,6 +45,9 @@ - 增强引用提示面板、段落节奏、代码块、表格与行内代码层次。 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/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/0010-fix-resolve-relative-page-links-from-source-notes.patch pnpm build pnpm lint ``` `0001`、`0002` 保留为早期两补丁拆分档案;只有在不使用 `0003` -原型系列时才单独应用。上述 `0003 → 0009` 顺序已于 2026-07-17 +原型系列时才单独应用。上述 `0003 → 0010` 顺序已于 2026-07-17 在服务器最新 `bingshuo/guanghu@514ab19` 的新克隆上完整复现。 ## 研发规则 diff --git a/apps/tolaria/patches/0010-fix-resolve-relative-page-links-from-source-notes.patch b/apps/tolaria/patches/0010-fix-resolve-relative-page-links-from-source-notes.patch new file mode 100644 index 0000000..4edce03 --- /dev/null +++ b/apps/tolaria/patches/0010-fix-resolve-relative-page-links-from-source-notes.patch @@ -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(null) +- useEditorLinkActivation(containerRef, onNavigateWikilink, vaultPath) ++ useEditorLinkActivation(containerRef, onNavigateWikilink, vaultPath, sourcePath) + return
+ } + +-function renderHarness(onNavigateWikilink = vi.fn(), vaultPath?: string) { +- render() ++function renderHarness(onNavigateWikilink = vi.fn(), vaultPath?: string, sourcePath?: string) { ++ render() + 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, + 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) + diff --git a/research/progress/HLP-DEV-0005-NOTION-READING-AND-PAGE-LINK-RESTORATION.md b/research/progress/HLP-DEV-0005-NOTION-READING-AND-PAGE-LINK-RESTORATION.md index b8a4d21..c331f52 100644 --- a/research/progress/HLP-DEV-0005-NOTION-READING-AND-PAGE-LINK-RESTORATION.md +++ b/research/progress/HLP-DEV-0005-NOTION-READING-AND-PAGE-LINK-RESTORATION.md @@ -30,6 +30,7 @@ Tolaria 页面路由的兼容层;当前页面还把部分参考路径放在代 - `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/0010-fix-resolve-relative-page-links-from-source-notes.patch` - 桌面验收包:`HoloLake Era 0.1.4 Review` - Bundle ID:`com.guanghu.desktop.review.20260717.page` - DMG SHA-256:`846884e8…c66bc303a` @@ -37,6 +38,7 @@ Tolaria 页面路由的兼容层;当前页面还把部分参考路径放在代 ## 验证 - 回归测试先失败后修复:相对 Markdown 页面链接普通单击跳转; +- 原生点击发现重复 `README.md` 会误跳后,再补回归测试:路径按来源页面目录归一化并跳到唯一正确页面; - 定向 Vitest:16 项通过; - ESLint、TypeScript 与 Vite production build 通过; - Tauri Apple Silicon App 与 DMG 构建通过;