From abeb618c12740e9b67058c6ea367ec230400e1ec Mon Sep 17 00:00:00 2001 From: Gamosoft Date: Mon, 5 Jan 2026 12:51:24 +0100 Subject: [PATCH] use wikilinks for images --- frontend/app.js | 84 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 10 deletions(-) diff --git a/frontend/app.js b/frontend/app.js index c6b9d86..8595b8b 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -164,6 +164,10 @@ function noteApp() { byEndPath: new Map(), // '/filename' and '/filename.md' -> true }, + // Image lookup map for O(1) image wikilink resolution (built on loadNotes) + // Maps image filename (case-insensitive) -> full path + _imageLookup: new Map(), + // Preview rendering debounce _previewDebounceTimeout: null, _lastRenderedContent: '', @@ -1004,12 +1008,25 @@ function noteApp() { this._noteLookup.byName.clear(); this._noteLookup.byNameLower.clear(); this._noteLookup.byEndPath.clear(); + this._imageLookup.clear(); for (const note of this.notes) { const path = note.path; const pathLower = path.toLowerCase(); const name = note.name; const nameLower = name.toLowerCase(); + + // Handle images separately - build image lookup map + if (note.type === 'image') { + // Map filename (case-insensitive) to full path + // First match wins if there are duplicates + if (!this._imageLookup.has(nameLower)) { + this._imageLookup.set(nameLower, path); + } + continue; + } + + // Notes only from here const nameWithoutMd = name.replace(/\.md$/i, ''); const nameWithoutMdLower = nameWithoutMd.toLowerCase(); @@ -1046,6 +1063,13 @@ function noteApp() { ); }, + // Resolve image wikilink to full path (O(1) lookup) + // Returns the full path if found, null otherwise + resolveImageWikilink(imageName) { + const nameLower = imageName.toLowerCase(); + return this._imageLookup.get(nameLower) || null; + }, + // Load all tags async loadTags() { try { @@ -1994,10 +2018,9 @@ function noteApp() { let link; if (isImage) { - // For images, insert image markdown - const filename = notePath.split('/').pop().replace(/\.[^/.]+$/, ''); // Remove extension - // Use relative path (not /api/images/) for portability - link = `![${filename}](${notePath})`; + // For images, use wiki-style link (resolves by filename, never breaks) + const filename = notePath.split('/').pop(); + link = `![[${filename}]]`; } else { // For notes, insert note link const noteName = notePath.split('/').pop().replace('.md', ''); @@ -2084,11 +2107,21 @@ function noteApp() { } }, - // Insert image markdown at cursor position + // Insert image markdown at cursor position using wiki-style syntax + // This ensures image links don't break when notes are moved insertImageMarkdown(imagePath, altText, cursorPos) { - const filename = altText.replace(/\.[^/.]+$/, ''); // Remove extension - // Use relative path (not /api/images/) for portability - const markdown = `![${filename}](${imagePath})`; + // Extract just the filename from the path (e.g., "folder/_attachments/image.png" -> "image.png") + const filename = imagePath.split('/').pop(); + + // Use wiki-style image link: ![[filename.png]] or ![[filename.png|alt text]] + // The alt text is optional - only add if different from filename + const filenameWithoutExt = filename.replace(/\.[^/.]+$/, ''); + const altWithoutExt = altText.replace(/\.[^/.]+$/, ''); + + // If alt text is meaningful (not just "pasted-image"), include it + const markdown = (altWithoutExt && altWithoutExt !== filenameWithoutExt && !altWithoutExt.startsWith('pasted-image')) + ? `![[${filename}|${altWithoutExt}]]` + : `![[${filename}]]`; const textBefore = this.noteContent.substring(0, cursorPos); const textAfter = this.noteContent.substring(cursorPos); @@ -2098,7 +2131,7 @@ function noteApp() { // Trigger autosave this.autoSave(); - // Reload notes to show the new image in sidebar + // Reload notes to show the new image in sidebar and update lookup maps this.loadNotes(); }, @@ -3763,7 +3796,38 @@ function noteApp() { return `\x00CODEBLOCK${codeBlocks.length - 1}\x00`; }); - // Step 2: Convert wiki links (now safe from code blocks) + // Step 2: Convert image wikilinks FIRST: ![[image.png]] or ![[image.png|alt text]] + // Must be before note wikilinks to prevent [[image.png]] from being matched first + contentToRender = contentToRender.replace( + /!\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, + (match, imageName, altText) => { + const filename = imageName.trim(); + const alt = altText ? altText.trim() : filename.replace(/\.[^/.]+$/, ''); + + // Resolve image path using O(1) lookup + const imagePath = self.resolveImageWikilink(filename); + + if (imagePath) { + // URL-encode path segments for the API + const encodedPath = imagePath.split('/').map(segment => { + try { + return encodeURIComponent(decodeURIComponent(segment)); + } catch (e) { + return encodeURIComponent(segment); + } + }).join('/'); + + const safeAlt = alt.replace(/"/g, '"'); + return `${safeAlt}`; + } + + // Image not found - return broken image indicator + const safeFilename = filename.replace(/&/g, '&').replace(//g, '>'); + return `🖼️ ${safeFilename}`; + } + ); + + // Step 2b: Convert note wikilinks: [[note]] or [[note|display text]] contentToRender = contentToRender.replace( /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (match, target, displayText) => {