package markdown import ( "bytes" "fmt" "path/filepath" "regexp" "strings" chromahtml "github.com/alecthomas/chroma/v2/formatters/html" "github.com/yuin/goldmark" highlighting "github.com/yuin/goldmark-highlighting/v2" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/renderer/html" "gobsidian/internal/config" ) type Renderer struct { md goldmark.Markdown config *config.Config } func NewRenderer(cfg *config.Config) *Renderer { md := goldmark.New( goldmark.WithExtensions( extension.GFM, extension.Table, extension.Strikethrough, extension.TaskList, highlighting.NewHighlighting( highlighting.WithStyle("github-dark"), highlighting.WithFormatOptions( chromahtml.WithLineNumbers(true), chromahtml.WithClasses(true), ), ), ), goldmark.WithParserOptions( parser.WithAutoHeadingID(), ), goldmark.WithRendererOptions( html.WithHardWraps(), html.WithXHTML(), html.WithUnsafe(), ), ) return &Renderer{ md: md, config: cfg, } } func (r *Renderer) RenderMarkdown(content string, notePath string) (string, error) { // Process Obsidian image syntax content = r.processObsidianImages(content, notePath) // Process Obsidian links content = r.processObsidianLinks(content) var buf bytes.Buffer if err := r.md.Convert([]byte(content), &buf); err != nil { return "", err } return buf.String(), nil } func (r *Renderer) processObsidianImages(content string, notePath string) string { // Regex to match Obsidian image syntax: ![[image.png]] or ![[folder/image.png]] obsidianImageRegex := regexp.MustCompile(`!\[\[([^\]]+\.(png|jpg|jpeg|gif|webp))\]\]`) return obsidianImageRegex.ReplaceAllStringFunc(content, func(match string) string { // Extract the image path from the match submatch := obsidianImageRegex.FindStringSubmatch(match) if len(submatch) < 2 { return match } imagePath := submatch[1] // Get storage info based on current note path // storageInfo := utils.GetImageStorageInfo(notePath, r.config) // Clean up the path cleanPath := filepath.Clean(imagePath) cleanPath = strings.ReplaceAll(cleanPath, "\\", "/") cleanPath = strings.Trim(cleanPath, "/") // Generate the appropriate URL based on storage mode var imageURL string switch r.config.ImageStorageMode { case 2: // Mode 2: Specific storage folder - just the filename imageURL = fmt.Sprintf("/serve_stored_image/%s", filepath.Base(cleanPath)) default: // For modes 1, 3, and 4 imageURL = fmt.Sprintf("/serve_attached_image/%s", cleanPath) } // Convert to standard markdown image syntax alt := filepath.Base(imagePath) return fmt.Sprintf("![%s](%s)", alt, imageURL) }) } func (r *Renderer) processObsidianLinks(content string) string { // Regex to match Obsidian wiki-links: [[Note Name]] or [[Note Name|Display Text]] obsidianLinkRegex := regexp.MustCompile(`\[\[([^\]|]+)(\|([^\]]+))?\]\]`) return obsidianLinkRegex.ReplaceAllStringFunc(content, func(match string) string { submatch := obsidianLinkRegex.FindStringSubmatch(match) if len(submatch) < 2 { return match } noteName := strings.TrimSpace(submatch[1]) displayText := noteName // If there's custom display text (after |), use it if len(submatch) >= 4 && submatch[3] != "" { displayText = strings.TrimSpace(submatch[3]) } // Convert note name to URL-friendly format noteURL := strings.ReplaceAll(noteName, " ", "%20") noteURL = fmt.Sprintf("/note/%s.md", noteURL) // Convert to standard markdown link syntax return fmt.Sprintf("[%s](%s)", displayText, noteURL) }) }