")
}
// processMediaEmbeds turns a bare URL on its own line into an embed element
// Supports: audio (mp3|wav|ogg), video (mp4|webm|ogg), pdf
func (r *Renderer) processMediaEmbeds(content string) string {
lines := strings.Split(content, "\n")
mediaRe := regexp.MustCompile(`^(https?://\S+\.(?:mp3|wav|ogg|mp4|webm|ogg|pdf))$`)
for i, ln := range lines {
trimmed := strings.TrimSpace(ln)
m := mediaRe.FindStringSubmatch(trimmed)
if len(m) == 0 {
continue
}
url := m[1]
switch {
case strings.HasSuffix(strings.ToLower(url), ".mp3") || strings.HasSuffix(strings.ToLower(url), ".wav") || strings.HasSuffix(strings.ToLower(url), ".ogg"):
lines[i] = fmt.Sprintf("", url)
case strings.HasSuffix(strings.ToLower(url), ".mp4") || strings.HasSuffix(strings.ToLower(url), ".webm") || strings.HasSuffix(strings.ToLower(url), ".ogg"):
lines[i] = fmt.Sprintf("", url)
case strings.HasSuffix(strings.ToLower(url), ".pdf"):
lines[i] = fmt.Sprintf("", url)
}
}
return strings.Join(lines, "\n")
}
func NewRenderer(cfg *config.Config) *Renderer {
md := goldmark.New(
goldmark.WithExtensions(
extension.GFM,
extension.Table,
extension.Strikethrough,
extension.TaskList,
extension.Footnote,
extension.DefinitionList,
extension.Linkify,
extension.Typographer,
emoji.Emoji,
highlighting.NewHighlighting(
highlighting.WithStyle("github-dark"),
highlighting.WithFormatOptions(
// Disable line numbers in code blocks for cleaner display
chromahtml.WithLineNumbers(false),
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)
// Convert Mermaid fenced code blocks to
for client rendering
content = r.processMermaidFences(content)
// Convert bare media links on their own line to embedded players (audio/video/pdf)
content = r.processMediaEmbeds(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)
}
// Prefix with configured URL base
if bp := r.config.URLPrefix; bp != "" {
imageURL = bp + imageURL
}
// Convert to standard markdown image syntax
alt := filepath.Base(imagePath)
return fmt.Sprintf("", 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)
if bp := r.config.URLPrefix; bp != "" {
noteURL = bp + noteURL
}
// Convert to standard markdown link syntax
return fmt.Sprintf("[%s](%s)", displayText, noteURL)
})
}