mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-09-19 17:37:29 +02:00
fix(paths): preserve cross-platform asset paths
This commit is contained in:
@@ -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 @@
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 00-2 2v3M16 3h3a2 2 0 012 2v3M8 21H5a2 2 0 01-2-2v-3M16 21h3a2 2 0 002-2v-3"/></svg>
|
||||
</button>
|
||||
{/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')}
|
||||
<span class="img-toolbar-sep"></span>
|
||||
<button onclick={copyImageToClipboard} title="Copy image">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
|
||||
|
||||
@@ -27,6 +27,68 @@ export function relativePath(fromDirectory: string, targetPath: string): string
|
||||
return result.join('/') || '.';
|
||||
}
|
||||
|
||||
export function normalizeLocalAssetPath(path: string): string {
|
||||
return path
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/^\/([A-Za-z]:\/)/, '$1');
|
||||
}
|
||||
|
||||
export function assetUrlToLocalPath(source: string): string | null {
|
||||
if (!source.startsWith('asset:') && !source.startsWith('http://asset.localhost') && !source.startsWith('https://asset.localhost')) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const path = normalizeLocalAssetPath(decodeURIComponent(new URL(source).pathname));
|
||||
// Tauri prepends a URL-path slash to POSIX and UNC paths.
|
||||
return path.startsWith('//') ? path.substring(1) : path;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveVaultFilePath(
|
||||
targetPath: string,
|
||||
notePath: string | null,
|
||||
vaultRoot: string | null,
|
||||
): string {
|
||||
const target = normalizeLocalAssetPath(targetPath);
|
||||
if (target.startsWith('/') || /^[A-Za-z]:\//.test(target)) return target;
|
||||
|
||||
const root = vaultRoot?.replace(/\\/g, '/').replace(/\/$/, '') ?? '';
|
||||
if (target.startsWith('.helixnotes/') && root) {
|
||||
return resolvePathFromFile(`${root}/.vault-root`, target);
|
||||
}
|
||||
if (notePath) return resolvePathFromFile(notePath, target);
|
||||
if (root) return resolvePathFromFile(`${root}/.vault-root`, target);
|
||||
return target;
|
||||
}
|
||||
|
||||
export function assetSourceToMarkdown(
|
||||
source: string,
|
||||
notePath: string | null,
|
||||
vaultRoot: string | null,
|
||||
): string {
|
||||
if (source.startsWith('blob:')) return '';
|
||||
if (source.startsWith('imgproxy:') || source.startsWith('http://imgproxy.localhost') || source.startsWith('https://imgproxy.localhost')) {
|
||||
try {
|
||||
return decodeURIComponent(new URL(source).pathname.substring(1));
|
||||
} catch {
|
||||
return source;
|
||||
}
|
||||
}
|
||||
const absolutePath = assetUrlToLocalPath(source);
|
||||
if (absolutePath === null) return source;
|
||||
|
||||
const normalizedRoot = vaultRoot?.replace(/\\/g, '/').replace(/\/$/, '');
|
||||
if (!normalizedRoot || !absolutePath.startsWith(normalizedRoot + '/')) return absolutePath;
|
||||
const vaultRelative = absolutePath.substring(normalizedRoot.length + 1);
|
||||
if (vaultRelative.startsWith('.helixnotes/')) return vaultRelative;
|
||||
if (!notePath) return vaultRelative;
|
||||
const normalizedNotePath = notePath.replace(/\\/g, '/');
|
||||
const noteDirectory = normalizedNotePath.substring(0, normalizedNotePath.lastIndexOf('/'));
|
||||
return relativePath(noteDirectory, absolutePath);
|
||||
}
|
||||
|
||||
export function resolvePathFromFile(filePath: string, targetPath: string): string {
|
||||
const normalizedFile = filePath.replace(/\\/g, '/');
|
||||
const normalizedTarget = targetPath.replace(/\\/g, '/');
|
||||
|
||||
Reference in New Issue
Block a user