diff --git a/main.go b/main.go
index 33edd2f..cdbf268 100644
--- a/main.go
+++ b/main.go
@@ -2,6 +2,7 @@ package main
import (
"crypto/rand"
+ "encoding/base64"
_ "embed"
"encoding/hex"
"errors"
@@ -11,6 +12,7 @@ import (
"log"
"os"
"path/filepath"
+ "regexp"
"strings"
"github.com/pkg/browser"
@@ -19,6 +21,10 @@ import (
var appVersion string
+// imgSrcRegex matches
tags with src attributes
+// Captures: 1=prefix, 2=opening quote, 3=src path, 4=closing quote
+var imgSrcRegex = regexp.MustCompile(`(
]*\ssrc=)(["']?)([^"'\s>]+)(["']?)`)
+
//go:embed github-markdown.css
var style string
@@ -58,6 +64,11 @@ func main() {
markdown.Typographer(true))
markdownTokens := md.Parse(dat)
+
+ // Convert relative image links to data URIs
+ baseDir := filepath.Dir(inputFilename)
+ processImageTokens(markdownTokens, baseDir)
+
html := md.RenderTokensToString(markdownTokens)
title := getTitle(markdownTokens)
@@ -172,3 +183,179 @@ func getText(token markdown.Token) string {
func isSnap() bool {
return os.Getenv("SNAP_USER_COMMON") != ""
}
+
+// processImageTokens walks through markdown tokens and converts relative image paths to data URIs
+func processImageTokens(tokens []markdown.Token, baseDir string) {
+ for _, token := range tokens {
+ switch t := token.(type) {
+ case *markdown.Image:
+ if isRelativePath(t.Src) {
+ if dataURI := imageToDataURI(t.Src, baseDir); dataURI != "" {
+ t.Src = dataURI
+ }
+ }
+ case *markdown.HTMLInline:
+ // Process inline HTML that may contain
tags
+ t.Content = processHTMLImages(t.Content, baseDir)
+ case *markdown.HTMLBlock:
+ // Process block HTML that may contain
tags
+ t.Content = processHTMLImages(t.Content, baseDir)
+ case *markdown.Inline:
+ // Recursively process child tokens
+ if t.Children != nil {
+ processImageTokens(t.Children, baseDir)
+ }
+ }
+ }
+}
+
+// processHTMLImages processes HTML content and converts relative image src attributes to data URIs
+func processHTMLImages(html string, baseDir string) string {
+ // Use the package-level regex to match
tags with src attributes
+ result := imgSrcRegex.ReplaceAllStringFunc(html, func(match string) string {
+ // Extract the parts using the regex
+ parts := imgSrcRegex.FindStringSubmatch(match)
+ if len(parts) != 5 {
+ return match
+ }
+
+ prefix := parts[1] // " 3 {
+ log.Printf("Warning: Image path %s goes too many levels above base directory", imagePath)
+ return ""
+ }
+ }
+ }
+
+ // Check file size before reading (limit to 10MB to prevent memory issues)
+ fileInfo, err := os.Stat(cleanedPath)
+ if err != nil {
+ log.Printf("Warning: Unable to stat image file %s: %v", cleanedPath, err)
+ return ""
+ }
+
+ const maxSize = 10 * 1024 * 1024 // 10MB
+ if fileInfo.Size() > maxSize {
+ log.Printf("Warning: Image file %s is too large (%d bytes, max %d bytes)", cleanedPath, fileInfo.Size(), maxSize)
+ return ""
+ }
+
+ // Read the image file
+ data, err := os.ReadFile(cleanedPath)
+ if err != nil {
+ log.Printf("Warning: Unable to read image file %s: %v", cleanedPath, err)
+ return ""
+ }
+
+ // Determine MIME type based on file extension
+ mimeType := getMimeType(cleanedPath)
+
+ // Encode to base64
+ encoded := base64.StdEncoding.EncodeToString(data)
+
+ // Return data URI
+ return fmt.Sprintf("data:%s;base64,%s", mimeType, encoded)
+}
+
+// getMimeType returns the MIME type based on file extension
+func getMimeType(path string) string {
+ ext := strings.ToLower(filepath.Ext(path))
+ switch ext {
+ case ".jpg", ".jpeg":
+ return "image/jpeg"
+ case ".png":
+ return "image/png"
+ case ".gif":
+ return "image/gif"
+ case ".svg":
+ return "image/svg+xml"
+ case ".webp":
+ return "image/webp"
+ case ".bmp":
+ return "image/bmp"
+ case ".ico":
+ return "image/x-icon"
+ default:
+ // For unknown extensions, log a warning but try with generic image type
+ log.Printf("Warning: Unknown image extension %s for file %s, using image/* MIME type", ext, path)
+ return "image/*"
+ }
+}