217 lines
5 KiB
Go
217 lines
5 KiB
Go
// Package markdown defines a renderer for tview style tags.
|
|
package markdown
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/ayn2op/discordo/internal/config"
|
|
"github.com/diamondburned/ningen/v3/discordmd"
|
|
"github.com/yuin/goldmark/ast"
|
|
gmr "github.com/yuin/goldmark/renderer"
|
|
)
|
|
|
|
type Renderer struct {
|
|
theme config.MessagesListTheme
|
|
|
|
listIx *int
|
|
listNested int
|
|
}
|
|
|
|
func NewRenderer(theme config.MessagesListTheme) *Renderer {
|
|
return &Renderer{theme: theme}
|
|
}
|
|
|
|
func (r *Renderer) AddOptions(opts ...gmr.Option) {}
|
|
|
|
func (r *Renderer) Render(w io.Writer, source []byte, node ast.Node) error {
|
|
return ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
switch node := node.(type) {
|
|
case *ast.Document:
|
|
// noop
|
|
case *ast.Heading:
|
|
r.renderHeading(w, node, entering)
|
|
case *ast.Text:
|
|
r.renderText(w, node, entering, source)
|
|
case *ast.FencedCodeBlock:
|
|
r.renderFencedCodeBlock(w, node, entering, source)
|
|
case *ast.AutoLink:
|
|
r.renderAutoLink(w, node, entering, source)
|
|
case *ast.Link:
|
|
r.renderLink(w, node, entering)
|
|
case *ast.List:
|
|
r.renderList(w, node, entering)
|
|
case *ast.ListItem:
|
|
r.renderListItem(w, entering)
|
|
|
|
case *discordmd.Inline:
|
|
r.renderInline(w, node, entering)
|
|
case *discordmd.Mention:
|
|
r.renderMention(w, node, entering)
|
|
case *discordmd.Emoji:
|
|
r.renderEmoji(w, node, entering)
|
|
}
|
|
|
|
return ast.WalkContinue, nil
|
|
})
|
|
}
|
|
|
|
func (r *Renderer) renderHeading(w io.Writer, node *ast.Heading, entering bool) {
|
|
if entering {
|
|
io.WriteString(w, strings.Repeat("#", node.Level))
|
|
io.WriteString(w, " ")
|
|
} else {
|
|
io.WriteString(w, "\n")
|
|
}
|
|
}
|
|
|
|
func (r *Renderer) renderFencedCodeBlock(w io.Writer, node *ast.FencedCodeBlock, entering bool, source []byte) {
|
|
io.WriteString(w, "\n")
|
|
|
|
if entering {
|
|
// language
|
|
if l := node.Language(source); l != nil {
|
|
io.WriteString(w, "|=> ")
|
|
w.Write(l)
|
|
io.WriteString(w, "\n")
|
|
}
|
|
|
|
// body
|
|
lines := node.Lines()
|
|
for i := range lines.Len() {
|
|
line := lines.At(i)
|
|
io.WriteString(w, "| ")
|
|
w.Write(line.Value(source))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *Renderer) renderAutoLink(w io.Writer, node *ast.AutoLink, entering bool, source []byte) {
|
|
urlStyle := r.theme.URLStyle
|
|
|
|
if entering {
|
|
fg := urlStyle.GetForeground()
|
|
bg := urlStyle.GetBackground()
|
|
fmt.Fprintf(w, "[%s:%s]", fg, bg)
|
|
w.Write(node.URL(source))
|
|
} else {
|
|
io.WriteString(w, "[-:-]")
|
|
}
|
|
}
|
|
|
|
func (r *Renderer) renderLink(w io.Writer, node *ast.Link, entering bool) {
|
|
urlStyle := r.theme.URLStyle
|
|
if entering {
|
|
fg := urlStyle.GetForeground()
|
|
bg := urlStyle.GetBackground()
|
|
fmt.Fprintf(w, "[%s:%s::%s]", fg, bg, node.Destination)
|
|
} else {
|
|
io.WriteString(w, "[-:-::-]")
|
|
}
|
|
}
|
|
|
|
func (r *Renderer) renderList(w io.Writer, node *ast.List, entering bool) {
|
|
if node.IsOrdered() {
|
|
r.listIx = &node.Start
|
|
} else {
|
|
r.listIx = nil
|
|
}
|
|
|
|
if entering {
|
|
io.WriteString(w, "\n")
|
|
r.listNested++
|
|
} else {
|
|
r.listNested--
|
|
}
|
|
}
|
|
|
|
func (r *Renderer) renderListItem(w io.Writer, entering bool) {
|
|
if entering {
|
|
io.WriteString(w, strings.Repeat(" ", r.listNested-1))
|
|
|
|
if r.listIx != nil {
|
|
io.WriteString(w, strconv.Itoa(*r.listIx))
|
|
io.WriteString(w, ". ")
|
|
*r.listIx++
|
|
} else {
|
|
io.WriteString(w, "- ")
|
|
}
|
|
} else {
|
|
io.WriteString(w, "\n")
|
|
}
|
|
}
|
|
|
|
func (r *Renderer) renderText(w io.Writer, node *ast.Text, entering bool, source []byte) {
|
|
if entering {
|
|
w.Write(node.Segment.Value(source))
|
|
switch {
|
|
case node.HardLineBreak():
|
|
io.WriteString(w, "\n\n")
|
|
case node.SoftLineBreak():
|
|
io.WriteString(w, "\n")
|
|
}
|
|
}
|
|
}
|
|
func (r *Renderer) renderInline(w io.Writer, node *discordmd.Inline, entering bool) {
|
|
if start, end := attrToTag(node.Attr); entering && start != "" {
|
|
io.WriteString(w, start)
|
|
} else {
|
|
io.WriteString(w, end)
|
|
}
|
|
}
|
|
|
|
func (r *Renderer) renderMention(w io.Writer, node *discordmd.Mention, entering bool) {
|
|
mentionStyle := r.theme.MentionStyle
|
|
if entering {
|
|
fg := mentionStyle.GetForeground()
|
|
bg := mentionStyle.GetBackground()
|
|
fmt.Fprintf(w, "[%s:%s:b]", fg, bg)
|
|
|
|
switch {
|
|
case node.Channel != nil:
|
|
io.WriteString(w, "#"+node.Channel.Name)
|
|
case node.GuildUser != nil:
|
|
name := node.GuildUser.DisplayOrUsername()
|
|
if member := node.GuildUser.Member; member != nil && member.Nick != "" {
|
|
name = member.Nick
|
|
}
|
|
|
|
io.WriteString(w, "@"+name)
|
|
case node.GuildRole != nil:
|
|
io.WriteString(w, "@"+node.GuildRole.Name)
|
|
}
|
|
} else {
|
|
io.WriteString(w, "[-:-:B]")
|
|
}
|
|
}
|
|
|
|
func (r *Renderer) renderEmoji(w io.Writer, node *discordmd.Emoji, entering bool) {
|
|
if entering {
|
|
emojiStyle := r.theme.EmojiStyle
|
|
fg := emojiStyle.GetForeground()
|
|
bg := emojiStyle.GetBackground()
|
|
fmt.Fprintf(w, "[%s:%s]", fg, bg)
|
|
io.WriteString(w, ":"+node.Name+":")
|
|
} else {
|
|
io.WriteString(w, "[-:-]")
|
|
}
|
|
}
|
|
|
|
func attrToTag(attr discordmd.Attribute) (string, string) {
|
|
switch attr {
|
|
case discordmd.AttrBold:
|
|
return "[::b]", "[::B]"
|
|
case discordmd.AttrItalics:
|
|
return "[::i]", "[::I]"
|
|
case discordmd.AttrUnderline:
|
|
return "[::u]", "[::U]"
|
|
case discordmd.AttrStrikethrough:
|
|
return "[::s]", "[::S]"
|
|
case discordmd.AttrMonospace:
|
|
return "[::r]", "[::R]"
|
|
default:
|
|
return "", ""
|
|
}
|
|
}
|