Compare commits

...
3 Commits
Author SHA1 Message Date
Yuri Karamian 43f15962f7 Revert "chore(release): prepare v1.3.5"
This reverts commit ade146f9f3.
2026-08-17 17:03:01 +02:00
Yuri Karamian ade146f9f3 chore(release): prepare v1.3.5 2026-08-17 16:50:14 +02:00
Yuri Karamian b253ec4121 fix(paths): preserve cross-platform asset paths 2026-08-17 15:58:20 +02:00
3 changed files with 90 additions and 125 deletions
+22 -98
View File
@@ -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>
+62
View File
@@ -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, '/');
+6 -27
View File
@@ -7,6 +7,7 @@
import { darkThemes, isMobile, isAndroid } from '$lib/platform';
import type { CustomTheme } from '$lib/types';
import ResizeHandles from '$lib/components/ResizeHandles.svelte';
import { resolveVaultFilePath } from '$lib/utils/paths';
let { children } = $props();
@@ -73,16 +74,6 @@
}
}
function normalizePath(p: string): string {
const parts = p.split('/');
const resolved: string[] = [];
for (const seg of parts) {
if (seg === '..') resolved.pop();
else if (seg !== '.') resolved.push(seg);
}
return resolved.join('/');
}
function openLocalFile(path: string) {
if (isAndroid) {
const bridge = (window as any).Android;
@@ -98,24 +89,12 @@
if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('mailto:') || href.startsWith('tel:') || href.startsWith('sms:')) {
openUrl(href).catch((err) => console.error('Failed to open URL:', err));
} else if (!href.startsWith('#')) {
const decoded = decodeURIComponent(href);
const config = get(appConfig);
const vaultRoot = config?.active_vault;
let absPath = decoded;
if (!decoded.startsWith('/') && vaultRoot) {
// .helixnotes/ paths are always relative to vault root, not the note's directory
if (decoded.startsWith('.helixnotes/')) {
absPath = normalizePath(`${vaultRoot}/${decoded}`);
} else {
const notePath = get(activeNotePath);
if (notePath) {
const noteDir = notePath.substring(0, notePath.lastIndexOf('/'));
absPath = normalizePath(`${noteDir}/${decoded}`);
} else {
absPath = normalizePath(`${vaultRoot}/${decoded}`);
}
}
}
const absPath = resolveVaultFilePath(
decodeURIComponent(href),
get(activeNotePath),
config?.active_vault ?? null,
);
// Internal .md note link - navigate within the app
if (absPath.endsWith('.md')) {
readNote(absPath).then((content) => {