Files
gobsidian/internal/markdown/renderer.go

135 lines
3.6 KiB
Go
Raw Normal View History

2025-08-25 08:48:52 +01:00
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(
2025-08-25 17:26:27 +01:00
// Disable line numbers in code blocks for cleaner display
chromahtml.WithLineNumbers(false),
2025-08-25 08:48:52 +01:00
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)
})
}