From b253ec412139dcb4c31b5f970ba298790f346df3 Mon Sep 17 00:00:00 2001 From: Yuri Karamian Date: Mon, 17 Aug 2026 15:58:20 +0200 Subject: [PATCH] fix(paths): preserve cross-platform asset paths --- src/lib/components/Editor.svelte | 120 ++++++------------------------- src/lib/utils/paths.ts | 62 ++++++++++++++++ src/routes/+layout.svelte | 33 ++------- 3 files changed, 90 insertions(+), 125 deletions(-) diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index 62e9026..420c302 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -54,7 +54,7 @@ import { convertListNode, type MixedListName } from '$lib/editor/mixedLists'; import { clearFormatting } from '$lib/editor/clearFormatting'; import { serializeInlineMarkdown } from '$lib/editor/markdown'; - import { relativePath, resolvePathFromFile } from '$lib/utils/paths'; + import { assetSourceToMarkdown, assetUrlToLocalPath, normalizeLocalAssetPath, resolveVaultFilePath } from '$lib/utils/paths'; import GraphView from './GraphView.svelte'; import TagSuggestInput from './TagSuggestInput.svelte'; import ImageViewer from './ImageViewer.svelte'; @@ -2974,13 +2974,11 @@ if (src.startsWith('http://') || src.startsWith('https://')) { return convertFileSrc(src, 'imgproxy'); } - // Decode percent-encoding (%20 → space, etc.) for filesystem resolution - let decoded = decodeURIComponent(src); - // Fix multiple leading slashes (from broken saves) - if (decoded.match(/^\/{2,}/)) { - decoded = decoded.replace(/^\/{2,}/, '/'); - } - if (decoded.startsWith('/')) { + // Decode URL encoding and undo the extra leading slash added to Windows drive paths. + let decoded = normalizeLocalAssetPath(decodeURIComponent(src)); + // Keep repairing legacy POSIX paths that were saved with duplicate leading slashes. + if (/^\/{2,}/.test(decoded)) decoded = decoded.replace(/^\/{2,}/, '/'); + if (decoded.startsWith('/') || /^[A-Za-z]:\//.test(decoded)) { return convertFileSrc(normalizePath(decoded)); } // Paths containing .helixnotes/ are vault-root relative (our own attachments) @@ -3925,42 +3923,7 @@ } function stripAssetSrc(src: string): string { - // blob: URLs are not persistable - they were temporary browser references - if (src.startsWith('blob:')) return ''; - // Convert imgproxy:// URLs back to original external URLs for saving - if (src.startsWith('imgproxy:') || src.startsWith('http://imgproxy.localhost') || src.startsWith('https://imgproxy.localhost')) { - try { - const url = new URL(src); - return decodeURIComponent(url.pathname.substring(1)); - } catch { - return src; - } - } - // Convert asset:// URLs back to relative paths for saving - if (!src.startsWith('asset:') && !src.startsWith('http://asset.localhost') && !src.startsWith('https://asset.localhost')) return src; - let absPath = ''; - try { - const url = new URL(src); - absPath = decodeURIComponent(url.pathname); - } catch { - return src; - } - // Clean up any leading double/triple slashes (URL parsing artifact) - absPath = absPath.replace(/^\/{2,}/, '/'); - absPath = absPath.replace(/^\/([A-Za-z]:\/)/, '$1').replace(/\\/g, '/'); - const notePath = $activeNotePath; - const vaultRoot = $appConfig?.active_vault?.replace(/\\/g, '/').replace(/\/$/, ''); - if (vaultRoot && absPath.startsWith(vaultRoot + '/')) { - const vaultRelative = absPath.substring(vaultRoot.length + 1); - if (vaultRelative.startsWith('.helixnotes/')) return vaultRelative; - if (notePath) { - const normalizedNotePath = notePath.replace(/\\/g, '/'); - const noteDir = normalizedNotePath.substring(0, normalizedNotePath.lastIndexOf('/')); - return relativePath(noteDir, absPath); - } - return vaultRelative; - } - return absPath; + return assetSourceToMarkdown(src, $activeNotePath, $appConfig?.active_vault ?? null); } function htmlToMarkdown(html: string): string { @@ -4855,34 +4818,13 @@ } function getImageAbsPath(src: string): string { - // asset:// or http://asset.localhost → extract absolute path - if (src.startsWith('asset:') || src.startsWith('http://asset.localhost')) { - try { - const url = new URL(src); - let absPath = decodeURIComponent(url.pathname); - absPath = absPath.replace(/^\/{2,}/, '/'); - return absPath; - } catch { /* fall through */ } - } - // Relative path → resolve against note directory - let decoded = decodeURIComponent(src); - if (decoded.match(/^\/{2,}/)) decoded = decoded.replace(/^\/{2,}/, '/'); - if (decoded.startsWith('/')) return decoded; - if (decoded.includes('.helixnotes/')) { - const vaultRoot = $appConfig?.active_vault; - if (vaultRoot) { - const idx = decoded.indexOf('.helixnotes/'); - return `${vaultRoot}/${decoded.substring(idx)}`; - } - } - const notePath = $activeNotePath; - if (notePath) { - const noteDir = notePath.substring(0, notePath.lastIndexOf('/')); - return normalizePath(`${noteDir}/${decoded}`); - } - const vaultRoot = $appConfig?.active_vault; - if (vaultRoot) return normalizePath(`${vaultRoot}/${decoded}`); - return src; + const assetPath = assetUrlToLocalPath(src); + if (assetPath !== null) return assetPath; + return resolveVaultFilePath( + decodeURIComponent(src), + $activeNotePath, + $appConfig?.active_vault ?? null, + ); } async function copyImageToClipboard() { @@ -5535,17 +5477,8 @@ function resolveNoteHref(href: string): string | null { const decoded = decodeURIComponent(href); if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(decoded)) return null; - let absPath = decoded; - if (!decoded.startsWith('/')) { - const notePath = $activeNotePath; - if (notePath) { - absPath = resolvePathFromFile(notePath, decoded); - } else { - const vaultRoot = $appConfig?.active_vault; - if (vaultRoot) absPath = normalizePath(`${vaultRoot}/${decoded}`); - } - } - return absPath.endsWith('.md') ? absPath : null; + const path = resolveVaultFilePath(decoded, $activeNotePath, $appConfig?.active_vault ?? null); + return path.endsWith('.md') ? path : null; } function linkMenuOpen() { @@ -5609,20 +5542,11 @@ } function resolveHrefToAbsPath(href: string): string { - const decoded = decodeURIComponent(href); - if (decoded.startsWith('/')) return decoded; - // .helixnotes/ paths are always relative to vault root, not the note's directory - const vaultRoot = $appConfig?.active_vault; - if (decoded.startsWith('.helixnotes/') && vaultRoot) { - return normalizePath(`${vaultRoot}/${decoded}`); - } - const notePath = $activeNotePath; - if (notePath) { - const noteDir = notePath.substring(0, notePath.lastIndexOf('/')); - return normalizePath(`${noteDir}/${decoded}`); - } - if (vaultRoot) return normalizePath(`${vaultRoot}/${decoded}`); - return decoded; + return resolveVaultFilePath( + decodeURIComponent(href), + $activeNotePath, + $appConfig?.active_vault ?? null, + ); } function isFileLink(href: string): boolean { @@ -7363,7 +7287,7 @@ {/if} - {#if !isMobile && !imageToolbar.src.startsWith('imgproxy:') && !imageToolbar.src.startsWith('http://imgproxy.localhost')} + {#if !isMobile && !imageToolbar.src.startsWith('imgproxy:') && !imageToolbar.src.startsWith('http://imgproxy.localhost') && !imageToolbar.src.startsWith('https://imgproxy.localhost')}