html.go

  1package view
  2
  3import (
  4	"bytes"
  5	"fmt"
  6	"io"
  7	"mime/quotedprintable"
  8	"regexp"
  9	"strings"
 10
 11	"github.com/PuerkitoBio/goquery"
 12	"github.com/yuin/goldmark"
 13	"github.com/yuin/goldmark/renderer/html"
 14)
 15
 16// hyperlink formats a string as a terminal-clickable hyperlink.
 17func hyperlink(url, text string) string {
 18	if text == "" {
 19		text = url
 20	}
 21	return fmt.Sprintf("\x1b]8;;%s\x07%s\x1b]8;;\x07", url, text)
 22}
 23
 24func decodeQuotedPrintable(s string) (string, error) {
 25	reader := quotedprintable.NewReader(strings.NewReader(s))
 26	body, err := io.ReadAll(reader)
 27	if err != nil {
 28		return "", err
 29	}
 30	return string(body), nil
 31}
 32
 33// markdownToHTML converts a Markdown string to an HTML string.
 34func markdownToHTML(md []byte) []byte {
 35	var buf bytes.Buffer
 36	p := goldmark.New(
 37		goldmark.WithRendererOptions(
 38			html.WithUnsafe(), // Allow raw HTML in email.
 39		),
 40	)
 41	if err := p.Convert(md, &buf); err != nil {
 42		return md // Fallback to original markdown.
 43	}
 44	return buf.Bytes()
 45}
 46
 47// ProcessBody takes a raw email body, decodes it, and formats it as plain
 48// text with terminal hyperlinks.
 49func ProcessBody(rawBody string) (string, error) {
 50	decodedBody, err := decodeQuotedPrintable(rawBody)
 51	if err != nil {
 52		// If decoding fails, fallback to the raw body
 53		decodedBody = rawBody
 54	}
 55
 56	// Convert markdown to HTML before processing
 57	htmlBody := markdownToHTML([]byte(decodedBody))
 58
 59	doc, err := goquery.NewDocumentFromReader(bytes.NewReader(htmlBody))
 60	if err != nil {
 61		return "", fmt.Errorf("could not parse email body: %w", err)
 62	}
 63
 64	// Remove style and script tags to clean up the view
 65	doc.Find("style, script").Remove()
 66
 67	// Add newlines after block elements for better spacing
 68	doc.Find("p, div, h1, h2, h3, h4, h5, h6").Each(func(i int, s *goquery.Selection) {
 69		s.After("\n\n")
 70	})
 71
 72	// Replace <br> tags with newlines
 73	doc.Find("br").Each(func(i int, s *goquery.Selection) {
 74		s.ReplaceWithHtml("\n")
 75	})
 76
 77	// Format links into terminal hyperlinks
 78	doc.Find("a").Each(func(i int, s *goquery.Selection) {
 79		href, exists := s.Attr("href")
 80		if !exists {
 81			return
 82		}
 83		s.ReplaceWithHtml(hyperlink(href, s.Text()))
 84	})
 85
 86	// Format images into terminal hyperlinks
 87	doc.Find("img").Each(func(i int, s *goquery.Selection) {
 88		src, exists := s.Attr("src")
 89		if !exists {
 90			return
 91		}
 92		alt, _ := s.Attr("alt")
 93
 94		if alt == "" {
 95			alt = "Does not contain alt text"
 96		}
 97		s.ReplaceWithHtml(hyperlink(src, fmt.Sprintf("\n [Click here to view image: %s] \n", alt)))
 98	})
 99
100	// Get the document's text content, which now includes our formatting
101	text := doc.Text()
102
103	// Collapse more than 2 consecutive newlines into 2
104	re := regexp.MustCompile(`\n{3,}`)
105	text = re.ReplaceAllString(text, "\n\n")
106
107	return text, nil
108}