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/*" + } +}