shithub: hell

ref: 4f449e65176873defb69c5427f6fcd3c3f326a0c
dir: /renderer.go/

View raw version
package main

import (
	"bytes"
	"fmt"
	"strings"
	"math"

	"github.com/k3a/html2text"

	mastodon "codeberg.org/penny64/hellclient-go-mastodon"
	"golang.org/x/net/html"
)

type StatusFormatter struct {
	status      *mastodon.Status
	postContext *postref
	notif       *mastodon.Notification
	prefs       *Hellprefs
	// Index for posts that aren't using the ref in postref
	localindex string
}

// Returns the rendered content of a status's rt
func (st *StatusFormatter) reblogContent() string {
	currentpost := st.status
	defer func() { st.status = currentpost }()
	return st.statusContent(st.status.Reblog)
}

// Returns the rendered content of a status
func (st *StatusFormatter) statusContent(status *mastodon.Status) string {
	renderedPost, plaintexts := st.postContext.renderStatus(status.Content, st.postContext.prefix+st.postContext.ref)
	renderedPost = html2text.HTML2TextWithOptions(renderedPost, html2text.WithUnixLineBreaks(), html2text.WithListSupport())
	for key, plaintext := range plaintexts {
		renderedPost = strings.Replace(renderedPost, key, plaintext, 1)
	}
	poller := &poll_renderer{st}
	return renderedPost + poller.String()
}

func (st *StatusFormatter) mediaDescriptions(reblog bool) string {
	var sb strings.Builder
	status := st.status
	mediaTag := st.prefs.MediaTag
	if reblog {
		status = status.Reblog
	}
	for _, item := range status.MediaAttachments {
		if item.Description != "" {
			sb.WriteString(fmt.Sprintf("\n%s [%s]", mediaTag, item.Description))
		} else {
			sb.WriteString(mediaTag)
		}
	}
	return sb.String()
}

func (st *StatusFormatter) username() string {
	return fmt.Sprintf("<%s>", st.status.Account.Username)
}

func (st *StatusFormatter) username_full() string {
	return fmt.Sprintf("<%s>", st.status.Account.Acct)
}
func (st *StatusFormatter) detailLine() string {
	var sb strings.Builder
	var items []string
	sb.WriteString(st.status.Visibility)
	sb.WriteString(" ")
	sb.WriteString(fmt.Sprintf("%v\n", st.status.CreatedAt.Local()))
	if !st.status.EditedAt.IsZero() {
		sb.WriteString(fmt.Sprintf("EDITED: %v\n", st.status.EditedAt.Local()))
	}
	if st.status.FavouritesCount > 0 {
		items = append(items, fmt.Sprintf("Likes:%v", st.status.FavouritesCount))
	}
	if st.status.ReblogsCount > 0 {
		items = append(items, fmt.Sprintf("Reblogs:%v", st.status.ReblogsCount))
	}
	if st.status.RepliesCount > 0 {
		items = append(items, fmt.Sprintf("Replies:%v", st.status.RepliesCount))
	}

	for i := range items {
		sb.WriteString(items[i])
		if i != len(items)-1 {
			sb.WriteString(" ")
		}
	}
	
	return sb.String()
}

// Stringer for current index
type indexString struct {
	*StatusFormatter
}

func (is *indexString) String() string {
	if is.localindex != "" {
		return fmt.Sprintf("%s>", is.localindex)
	}
	return fmt.Sprintf("%s%s>", is.postContext.prefix, is.postContext.ref)
}

// Stringer type for returning subjects or content
type statusOrContent struct {
	*StatusFormatter
}

// If status has a subject, return the subject
// Otherwise return the content
func (soc *statusOrContent) String() string {
	if soc.status.SpoilerText != "" {
		return fmt.Sprintf("[ %s ]", soc.status.SpoilerText)
	}
	return soc.statusContent(soc.status)
}

// Stringer for user who caused a notification
type notificationUser struct {
	*StatusFormatter
}

func (nu *notificationUser) String() string {
	return fmt.Sprintf("[%s]", nu.notif.Account.Acct)
}

// Stringer for type of notification
type notificationType struct {
	*StatusFormatter
}

func (nt *notificationType) String() string {
	return fmt.Sprintf("[%s]", nt.notif.Type)
}

// Status content stringer
type statusContent struct {
	*StatusFormatter
}

func (cf *statusContent) String() string {
	return cf.statusContent(cf.status)
}

// Status boost content stringer
type boostContent struct {
	*StatusFormatter
}

func (bc *boostContent) String() string {
	return bc.reblogContent()
}

// Media descriptions for boosted posts stringer
type boostMediaDescriptions struct {
	*StatusFormatter
}

func (bm *boostMediaDescriptions) String() string {
	return bm.mediaDescriptions(true)
}

// Media description stringer
type mediaDescriptions struct {
	*StatusFormatter
}

func (md *mediaDescriptions) String() string {
	return md.mediaDescriptions(false)
}

// Post detail line (likes rts replies) stringer
type detailLine struct {
	*StatusFormatter
}

func (dl *detailLine) String() string {
	return dl.detailLine()
}

// Boosted username stringer
type boostedusername struct {
	*StatusFormatter
}

func (usr *boostedusername) String() string {
	var sb strings.Builder
	sb.WriteString("<")
	sb.WriteString(usr.status.Reblog.Account.Username)
	sb.WriteString(">")
	return sb.String()
}

// Display name stringer
type display_name struct {
	*StatusFormatter
}

func (usr *display_name) String() string {
	return usr.status.Account.DisplayName
}

// Formatted <username> stringer
type username struct {
	*StatusFormatter
}

func (usr *username) String() string {
	return usr.username()
}

// Formatted <username@domain> stringer
type username_full struct {
	*StatusFormatter
}

func (usr *username_full) String() string {
	return usr.username_full()
}

type poll_renderer struct {
	*StatusFormatter
}

func (poll *poll_renderer) String() string {
	if poll.status.Poll != nil {
		var sb strings.Builder
		sb.WriteString("\n")
		total := poll.status.Poll.VotesCount
		for _, item := range poll.status.Poll.Options {
			sb.WriteString(item.Title)
			sb.WriteString(" ")
			percent := math.Round(float64(item.VotesCount) / float64(total) * 100)
			sb.WriteString(fmt.Sprintf("%v%%\n", percent))
		}
		return sb.String()
	}
	return ""
}

func (pr *postref) renderStatus(content string, index string) (string, map[string]string) {
	doc, err := html.Parse(strings.NewReader(content))
	if err != nil {
		fmt.Printf("Failed to parse status\n")
		return "", nil
	}

	//clear out the url map
	pr.urlmap[index] = []string{}
	preformats := make(map[string]string)

	for node := range doc.Descendants() {
		if (node.Data == "pre" || node.Data == "") && node.FirstChild != nil {
			preformats[fmt.Sprintf("%p%p ", pr, node.FirstChild)] = node.FirstChild.Data
			node.FirstChild.Data = fmt.Sprintf("%p%p ", pr, node.FirstChild)
		}
		if node.Data == "a" && node.Type == html.ElementNode {
			ismention := false
			ishashtag := false
			href := ""

			for attr := range node.Attr {
				if node.Attr[attr].Key == "class" && strings.Contains(node.Attr[attr].Val, "mention") {
					node.Data = "div"
					if !strings.Contains(node.Attr[attr].Val, "hashtag") {
						ismention = true
					} else {
						ishashtag = true
					}
					continue
				}
				if node.Attr[attr].Key == "href" {
					href = node.Attr[attr].Val
					//Replace the href with the description if the URL has one
					if node.FirstChild != nil && node.FirstChild.Type == html.TextNode && !ismention {
						node.Attr[attr].Val = fmt.Sprintf("(%s)", node.FirstChild.Data)
					}
				}
			}
			if !ismention && !ishashtag && href != "" {
				pr.urlmap[index] = append(pr.urlmap[index], href)
				refnode := &html.Node{
					Type: html.TextNode,
					Data: fmt.Sprintf(" [%v]", len(pr.urlmap[index]))}
				if node.Parent != nil {
					node.Parent.InsertBefore(refnode, node.NextSibling)
				}
			}
		}
	}

	//Rip off the HTML body the parser made for us
	for node := range doc.Descendants() {
		if node.Data == "body" {
			node.Type = html.DocumentNode
			doc = node
		}
	}

	var rendered bytes.Buffer
	err = html.Render(&rendered, doc)

	if err != nil {
		return "", nil
	}

	renderedPlainText := rendered.String()

	return renderedPlainText, preformats
}