diff --git a/cmd/mdv-gui/app.go b/cmd/mdv-gui/app.go index d57999e..d8a6d15 100644 --- a/cmd/mdv-gui/app.go +++ b/cmd/mdv-gui/app.go @@ -61,7 +61,7 @@ func (a *App) load(path string) error { if err != nil { return err } - htmlBytes, err := render.ToHTML(b, a.cfg.GUITheme, a.cfg.GUIThemeLight, a.cfg.GUIThemeDark, a.cfg.GUIWidth) + htmlBytes, err := render.ToHTML(b, a.cfg.GUITheme, a.cfg.GUIThemeLight, a.cfg.GUIThemeDark, a.cfg.GUIWidth, path) if err != nil { return err } diff --git a/examples/images-test.md b/examples/images-test.md new file mode 100644 index 0000000..a6559d6 --- /dev/null +++ b/examples/images-test.md @@ -0,0 +1,25 @@ +# Image Rendering Test + +This document tests local image rendering in mdv-gui. + +## Local Image References + +### Relative Path +Here's an image using a relative path: + +![Test Icon](images/test-icon.png) + +### Another Reference +This should display the mdv app icon: + +![MDV Icon](./images/test-icon.png) + +## External Images + +For comparison, here's an external image (should work without changes): + +![External](https://avatars.githubusercontent.com/u/764612?s=80&v=4) + +## Test Results + +If you can see the local images above in mdv-gui, the feature is working correctly! diff --git a/examples/images/test-icon.png b/examples/images/test-icon.png new file mode 100644 index 0000000..22e0765 Binary files /dev/null and b/examples/images/test-icon.png differ diff --git a/internal/render/golden_test.go b/internal/render/golden_test.go index 6cbf14e..589dc8e 100644 --- a/internal/render/golden_test.go +++ b/internal/render/golden_test.go @@ -11,7 +11,7 @@ func TestToHTMLMatchesGolden(t *testing.T) { src := []byte("# Title\n\nContent") themePath := filepath.Join("testdata", "html_theme.css") - out, err := ToHTML(src, themePath, "", "", "800") + out, err := ToHTML(src, themePath, "", "", "800", "") if err != nil { t.Fatalf("ToHTML returned error: %v", err) } diff --git a/internal/render/render.go b/internal/render/render.go index 6778630..5a21de7 100644 --- a/internal/render/render.go +++ b/internal/render/render.go @@ -2,10 +2,12 @@ package render import ( "bytes" + "encoding/base64" "fmt" "os" "os/exec" "path/filepath" + "regexp" "runtime" "strconv" "strings" @@ -122,7 +124,76 @@ func ResolveTheme(theme, themeLight, themeDark string) string { return detected } -func ToHTML(src []byte, theme, themeLight, themeDark, width string) ([]byte, error) { +// processLocalImages processes HTML and converts relative image paths to data URIs. +// basePath is the directory of the markdown file, used to resolve relative paths. +func processLocalImages(htmlContent string, basePath string) string { + // Regex to match img tags with src attributes + imgRegex := regexp.MustCompile(`]*?)src=["']([^"']+)["']([^>]*?)>`) + + return imgRegex.ReplaceAllStringFunc(htmlContent, func(match string) string { + // Extract the src attribute value + srcRegex := regexp.MustCompile(`src=["']([^"']+)["']`) + srcMatches := srcRegex.FindStringSubmatch(match) + if len(srcMatches) < 2 { + return match + } + + src := srcMatches[1] + + // Skip if it's already a data URI or absolute URL + if strings.HasPrefix(src, "data:") || + strings.HasPrefix(src, "http://") || + strings.HasPrefix(src, "https://") || + strings.HasPrefix(src, "//") { + return match + } + + // Resolve relative path to absolute path + var absPath string + if filepath.IsAbs(src) { + absPath = src + } else { + absPath = filepath.Join(basePath, src) + } + + // Read the image file + imgData, err := os.ReadFile(absPath) + if err != nil { + // If we can't read the file, return the original tag + return match + } + + // Determine MIME type based on file extension + ext := strings.ToLower(filepath.Ext(absPath)) + mimeType := "image/png" // default + switch ext { + case ".jpg", ".jpeg": + mimeType = "image/jpeg" + case ".png": + mimeType = "image/png" + case ".gif": + mimeType = "image/gif" + case ".svg": + mimeType = "image/svg+xml" + case ".webp": + mimeType = "image/webp" + case ".bmp": + mimeType = "image/bmp" + case ".ico": + mimeType = "image/x-icon" + } + + // Encode to base64 + encoded := base64.StdEncoding.EncodeToString(imgData) + dataURI := fmt.Sprintf("data:%s;base64,%s", mimeType, encoded) + + // Replace src attribute with data URI + newMatch := srcRegex.ReplaceAllString(match, fmt.Sprintf(`src="%s"`, dataURI)) + return newMatch + }) +} + +func ToHTML(src []byte, theme, themeLight, themeDark, width, mdFilePath string) ([]byte, error) { // Convert markdown to HTML var buf bytes.Buffer if err := md.Convert(src, &buf); err != nil { @@ -152,6 +223,13 @@ func ToHTML(src []byte, theme, themeLight, themeDark, width string) ([]byte, err widthCSS = fmt.Sprintf(".markdown-body { max-width: %s; margin-left: auto !important; margin-right: auto !important; }\n", maxWidth) } + // Process local images if we have a file path + htmlContent := buf.String() + if mdFilePath != "" { + basePath := filepath.Dir(mdFilePath) + htmlContent = processLocalImages(htmlContent, basePath) + } + // Wrap the HTML with the CSS theme and markdown-body container var output bytes.Buffer output.WriteString("\n") output.WriteString(`
` + "\n") - output.Write(buf.Bytes()) + output.WriteString(htmlContent) output.WriteString("\n
") return output.Bytes(), nil diff --git a/internal/render/render_test.go b/internal/render/render_test.go index 10238b7..2716369 100644 --- a/internal/render/render_test.go +++ b/internal/render/render_test.go @@ -110,7 +110,7 @@ func TestToHTMLWrapsOutputWithThemeAndWidth(t *testing.T) { t.Setenv("COLORFGBG", "15;0") src := []byte("# Title\n\nContent") - html, err := ToHTML(src, "auto", "", "", "narrow") + html, err := ToHTML(src, "auto", "", "", "narrow", "") if err != nil { t.Fatalf("ToHTML returned error: %v", err) }