html.go

 1package view
 2
 3import (
 4	"fmt"
 5	"io"
 6	"mime/quotedprintable"
 7	"regexp"
 8	"strings"
 9
10	"github.com/PuerkitoBio/goquery"
11)
12
13// hyperlink formats a string as a terminal-clickable hyperlink.
14func hyperlink(url, text string) string {
15	if text == "" {
16		text = url
17	}
18	return fmt.Sprintf("\x1b]8;;%s\x07%s\x1b]8;;\x07", url, text)
19}
20
21func decodeQuotedPrintable(s string) (string, error) {
22	reader := quotedprintable.NewReader(strings.NewReader(s))
23	body, err := io.ReadAll(reader)
24	if err != nil {
25		return "", err
26	}
27	return string(body), nil
28}
29
30// ProcessBody takes a raw email body, decodes it, and formats it as plain
31// text with terminal hyperlinks.
32func ProcessBody(rawBody string) (string, error) {
33	decodedBody, err := decodeQuotedPrintable(rawBody)
34	if err != nil {
35		// If decoding fails, fallback to the raw body
36		decodedBody = rawBody
37	}
38
39	doc, err := goquery.NewDocumentFromReader(strings.NewReader(decodedBody))
40	if err != nil {
41		return "", fmt.Errorf("could not parse email body: %w", err)
42	}
43
44	// Remove style and script tags to clean up the view
45	doc.Find("style, script").Remove()
46
47	// Add newlines after block elements for better spacing
48	doc.Find("p, div, h1, h2, h3, h4, h5, h6").Each(func(i int, s *goquery.Selection) {
49		s.After("\n\n")
50	})
51
52	// Replace <br> tags with newlines
53	doc.Find("br").Each(func(i int, s *goquery.Selection) {
54		s.ReplaceWithHtml("\n")
55	})
56
57	// Format links into terminal hyperlinks
58	doc.Find("a").Each(func(i int, s *goquery.Selection) {
59		href, exists := s.Attr("href")
60		if !exists {
61			return
62		}
63		s.ReplaceWithHtml(hyperlink(href, s.Text()))
64	})
65
66	// Get the text content, which now includes our formatting
67	text := doc.Text()
68
69	// Collapse more than 2 consecutive newlines into 2
70	re := regexp.MustCompile(`\n{3,}`)
71	text = re.ReplaceAllString(text, "\n\n")
72
73	return text, nil
74}