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	// Get the document's text content, which now includes our formatting
87	text := doc.Text()
88
89	// Collapse more than 2 consecutive newlines into 2
90	re := regexp.MustCompile(`\n{3,}`)
91	text = re.ReplaceAllString(text, "\n\n")
92
93	return text, nil
94}