diff --git a/CHANGELOG.md b/CHANGELOG.md index 68bd065a..67e6ae0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ - Added minimum tested Claude Code CLI version (2.1.78) to installation requirements (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/29) - Fixed anchor link clicks (`#section`) in markdown preview not enabling the Back button; Back now returns to the position before the anchor jump -- Fixed markdown preview not rendering images (`![alt](path)` syntax) +- Fixed markdown preview not rendering images with relative paths (`![alt](path)` syntax) - Fixed session tab blocking resize below ~600px when docked as a side panel (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/19) - Added alphabetical sorting to the "Select Project" dropdown (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/21) - Improved error panel on process start failure: shows Working Directory and CLAUDE_CONFIG_DIR fields (before Command and Error) for easier diagnosis; all fields have a right-click Copy menu; fixed "Restart Advanced…" not dismissing the error panel diff --git a/pom.xml b/pom.xml index 7b164bb2..c76667dd 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbclaudecodegui netbeans-claude-code-gui - 0.21.53-SNAPSHOT + 0.21.55-SNAPSHOT nbm Netbeans Claude Code GUI NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE. diff --git a/src/main/java/io/github/nbclaudecodegui/ui/MarkdownPreviewTab.java b/src/main/java/io/github/nbclaudecodegui/ui/MarkdownPreviewTab.java index 225fa90f..57ae04c2 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/MarkdownPreviewTab.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/MarkdownPreviewTab.java @@ -80,6 +80,9 @@ public class MarkdownPreviewTab extends TopComponent { /** Absolute path key used to identify this tab in {@link #OPEN_TABS}. */ String filePath; + /** Parent directory of {@link #filePath}; used to resolve relative image paths. */ + String baseDirPath; + // Navigation history final List historyPaths = new ArrayList<>(); final List historyFos = new ArrayList<>(); @@ -133,9 +136,10 @@ public static void openLive(String filePath, String content, FileObject fo) { MarkdownPreviewTab tab = new MarkdownPreviewTab(); tab.filePath = filePath; tab.fileObject = fo; + tab.baseDirPath = new java.io.File(filePath).getParent(); String html = MarkdownRenderer.toHtml(content != null ? content : ""); - tab.pane = MarkdownRenderer.createOutputPane(html, new java.io.File(filePath).getParent()); + tab.pane = MarkdownRenderer.createOutputPane(html, tab.baseDirPath); String name = new java.io.File(filePath).getName(); tab.setDisplayName("Preview: " + name); @@ -171,7 +175,8 @@ public static void openLive(String filePath, String content, FileObject fo) { * @param markdown markdown text to render */ public void updateContent(String markdown) { - String html = MarkdownRenderer.toHtml(markdown != null ? markdown : ""); + String html = MarkdownRenderer.resolveImagePaths( + MarkdownRenderer.toHtml(markdown != null ? markdown : ""), baseDirPath); SwingUtilities.invokeLater(() -> { if (pane == null) return; if (scrollPane == null) { @@ -298,6 +303,7 @@ void navigateTo(String newPath, FileObject newFo, boolean addToHistory) { } this.filePath = newPath; this.fileObject = newFo; + this.baseDirPath = new File(newPath).getParent(); OPEN_TABS.put(newPath, this); setDisplayName("Preview: " + new File(newPath).getName()); @@ -583,8 +589,9 @@ protected void componentOpened() { FileObject fo = FileUtil.toFileObject(f); if (pane == null) { try { + baseDirPath = f.getParent(); String content = fo != null ? fo.asText() : ""; - pane = MarkdownRenderer.createOutputPane(MarkdownRenderer.toHtml(content)); + pane = MarkdownRenderer.createOutputPane(MarkdownRenderer.toHtml(content), baseDirPath); } catch (IOException e) { return; } diff --git a/src/main/java/io/github/nbclaudecodegui/ui/common/MarkdownRenderer.java b/src/main/java/io/github/nbclaudecodegui/ui/common/MarkdownRenderer.java index 1ec95775..bb4d829e 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/common/MarkdownRenderer.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/common/MarkdownRenderer.java @@ -75,18 +75,28 @@ public static JEditorPane createOutputPane(String html) { */ public static JEditorPane createOutputPane(String html, String baseDirPath) { JEditorPane pane = createOutputPane(); - if (baseDirPath != null) { - try { - ((HTMLDocument) pane.getDocument()).setBase( - new java.net.URL("file://" + baseDirPath + "/")); - } catch (java.net.MalformedURLException ignored) { - // fall through — relative images won't resolve but nothing breaks - } - } - pane.setText(html); + pane.setText(resolveImagePaths(html, baseDirPath)); return pane; } + /** + * Replaces relative {@code src} attributes in {@code } tags with absolute + * {@code file://} URIs so Swing's HTMLEditorKit can load them regardless of the + * document base URL setting. + * + * @param html HTML fragment (may contain {@code } tags) + * @param baseDirPath absolute directory path of the file being rendered, or null + * @return the HTML with relative image paths resolved, or the original HTML if + * {@code baseDirPath} is null + */ + public static String resolveImagePaths(String html, String baseDirPath) { + if (baseDirPath == null) return html; + String base = baseDirPath.replace("\\", "/"); + return html.replaceAll( + "src=\"(?!https?://|file://|data:)([^\"]+)\"", + "src=\"file://" + base + "/$1\""); + } + /** * Creates and configures a {@link JEditorPane} for rendering chat output with no initial content. * diff --git a/src/test/java/io/github/nbclaudecodegui/ui/common/MarkdownRendererTest.java b/src/test/java/io/github/nbclaudecodegui/ui/common/MarkdownRendererTest.java index 287a6d36..443ad51c 100644 --- a/src/test/java/io/github/nbclaudecodegui/ui/common/MarkdownRendererTest.java +++ b/src/test/java/io/github/nbclaudecodegui/ui/common/MarkdownRendererTest.java @@ -265,4 +265,50 @@ void testHeadingSlugStripNonAlphanumeric() { String html = MarkdownRenderer.toHtml("# Hello, World! (2024)"); assertTrue(html.contains(""), "slug strips punctuation"); } + + // ------------------------------------------------------------------------- + // resolveImagePaths() + // ------------------------------------------------------------------------- + + @Test + void testResolveImagePathsRelative() { + String html = "\"\""; + String result = MarkdownRenderer.resolveImagePaths(html, "/docs"); + assertEquals("\"\"", result); + } + + @Test + void testResolveImagePathsAbsoluteHttpUnchanged() { + String html = "\"\""; + String result = MarkdownRenderer.resolveImagePaths(html, "/docs"); + assertEquals(html, result); + } + + @Test + void testResolveImagePathsFileUriUnchanged() { + String html = "\"\""; + String result = MarkdownRenderer.resolveImagePaths(html, "/docs"); + assertEquals(html, result); + } + + @Test + void testResolveImagePathsDataUriUnchanged() { + String html = "\"\""; + String result = MarkdownRenderer.resolveImagePaths(html, "/docs"); + assertEquals(html, result); + } + + @Test + void testResolveImagePathsNullBaseDirReturnsUnchanged() { + String html = "\"\""; + String result = MarkdownRenderer.resolveImagePaths(html, null); + assertEquals(html, result); + } + + @Test + void testResolveImagePathsWindowsBackslashesNormalized() { + String html = "\"\""; + String result = MarkdownRenderer.resolveImagePaths(html, "C:\\docs\\project"); + assertEquals("\"\"", result); + } }