renderer.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. // Package markdown defines a renderer for tview style tags.
  2. package markdown
  3. import (
  4. "fmt"
  5. "io"
  6. "strconv"
  7. "strings"
  8. "github.com/ayn2op/discordo/internal/config"
  9. "github.com/ayn2op/tview"
  10. "github.com/diamondburned/ningen/v3/discordmd"
  11. "github.com/yuin/goldmark/ast"
  12. gmr "github.com/yuin/goldmark/renderer"
  13. )
  14. type Renderer struct {
  15. theme config.MessagesListTheme
  16. listIx *int
  17. listNested int
  18. }
  19. func NewRenderer(theme config.MessagesListTheme) *Renderer {
  20. return &Renderer{theme: theme}
  21. }
  22. func (r *Renderer) AddOptions(opts ...gmr.Option) {}
  23. func (r *Renderer) Render(w io.Writer, source []byte, node ast.Node) error {
  24. return ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
  25. switch node := node.(type) {
  26. case *ast.Document:
  27. // noop
  28. case *ast.Heading:
  29. r.renderHeading(w, node, entering)
  30. case *ast.Text:
  31. r.renderText(w, node, entering, source)
  32. case *ast.FencedCodeBlock:
  33. r.renderFencedCodeBlock(w, node, entering, source)
  34. case *ast.AutoLink:
  35. r.renderAutoLink(w, node, entering, source)
  36. case *ast.Link:
  37. r.renderLink(w, node, entering)
  38. case *ast.List:
  39. r.renderList(w, node, entering)
  40. case *ast.ListItem:
  41. r.renderListItem(w, entering)
  42. case *discordmd.Inline:
  43. r.renderInline(w, node, entering)
  44. case *discordmd.Mention:
  45. r.renderMention(w, node, entering)
  46. case *discordmd.Emoji:
  47. r.renderEmoji(w, node, entering)
  48. }
  49. return ast.WalkContinue, nil
  50. })
  51. }
  52. func (r *Renderer) renderHeading(w io.Writer, node *ast.Heading, entering bool) {
  53. if entering {
  54. io.WriteString(w, strings.Repeat("#", node.Level))
  55. io.WriteString(w, " ")
  56. } else {
  57. io.WriteString(w, tview.NewLine)
  58. }
  59. }
  60. func (r *Renderer) renderFencedCodeBlock(w io.Writer, node *ast.FencedCodeBlock, entering bool, source []byte) {
  61. io.WriteString(w, tview.NewLine)
  62. if entering {
  63. // language
  64. if l := node.Language(source); l != nil {
  65. io.WriteString(w, "|=> ")
  66. w.Write(l)
  67. io.WriteString(w, tview.NewLine)
  68. }
  69. // body
  70. lines := node.Lines()
  71. for i := range lines.Len() {
  72. line := lines.At(i)
  73. io.WriteString(w, "| ")
  74. w.Write(line.Value(source))
  75. }
  76. }
  77. }
  78. func (r *Renderer) renderAutoLink(w io.Writer, node *ast.AutoLink, entering bool, source []byte) {
  79. urlStyle := r.theme.URLStyle
  80. if entering {
  81. fg, bg, _ := urlStyle.Decompose()
  82. fmt.Fprintf(w, "[%s:%s]", fg, bg)
  83. w.Write(node.URL(source))
  84. } else {
  85. io.WriteString(w, "[-:-]")
  86. }
  87. }
  88. func (r *Renderer) renderLink(w io.Writer, node *ast.Link, entering bool) {
  89. urlStyle := r.theme.URLStyle
  90. if entering {
  91. fg, bg, _ := urlStyle.Decompose()
  92. fmt.Fprintf(w, "[%s:%s::%s]", fg, bg, node.Destination)
  93. } else {
  94. io.WriteString(w, "[-:-::-]")
  95. }
  96. }
  97. func (r *Renderer) renderList(w io.Writer, node *ast.List, entering bool) {
  98. if node.IsOrdered() {
  99. r.listIx = &node.Start
  100. } else {
  101. r.listIx = nil
  102. }
  103. if entering {
  104. io.WriteString(w, tview.NewLine)
  105. r.listNested++
  106. } else {
  107. r.listNested--
  108. }
  109. }
  110. func (r *Renderer) renderListItem(w io.Writer, entering bool) {
  111. if entering {
  112. io.WriteString(w, strings.Repeat(" ", r.listNested-1))
  113. if r.listIx != nil {
  114. io.WriteString(w, strconv.Itoa(*r.listIx))
  115. io.WriteString(w, ". ")
  116. *r.listIx++
  117. } else {
  118. io.WriteString(w, "- ")
  119. }
  120. } else {
  121. io.WriteString(w, tview.NewLine)
  122. }
  123. }
  124. func (r *Renderer) renderText(w io.Writer, node *ast.Text, entering bool, source []byte) {
  125. if entering {
  126. w.Write(node.Segment.Value(source))
  127. switch {
  128. case node.HardLineBreak():
  129. io.WriteString(w, strings.Repeat(tview.NewLine, 2))
  130. case node.SoftLineBreak():
  131. io.WriteString(w, tview.NewLine)
  132. }
  133. }
  134. }
  135. func (r *Renderer) renderInline(w io.Writer, node *discordmd.Inline, entering bool) {
  136. if start, end := attrToTag(node.Attr); entering && start != "" {
  137. io.WriteString(w, start)
  138. } else {
  139. io.WriteString(w, end)
  140. }
  141. }
  142. func (r *Renderer) renderMention(w io.Writer, node *discordmd.Mention, entering bool) {
  143. mentionStyle := r.theme.MentionStyle
  144. if entering {
  145. fg, bg, _ := mentionStyle.Decompose()
  146. fmt.Fprintf(w, "[%s:%s:b]", fg, bg)
  147. switch {
  148. case node.Channel != nil:
  149. io.WriteString(w, "#"+node.Channel.Name)
  150. case node.GuildUser != nil:
  151. name := node.GuildUser.DisplayOrUsername()
  152. if member := node.GuildUser.Member; member != nil && member.Nick != "" {
  153. name = member.Nick
  154. }
  155. io.WriteString(w, "@"+name)
  156. case node.GuildRole != nil:
  157. io.WriteString(w, "@"+node.GuildRole.Name)
  158. }
  159. } else {
  160. io.WriteString(w, "[-:-:B]")
  161. }
  162. }
  163. func (r *Renderer) renderEmoji(w io.Writer, node *discordmd.Emoji, entering bool) {
  164. emojiStyle := r.theme.EmojiStyle
  165. if entering {
  166. fg, bg, _ := emojiStyle.Decompose()
  167. fmt.Fprintf(w, "[%s:%s]", fg, bg)
  168. io.WriteString(w, ":"+node.Name+":")
  169. } else {
  170. io.WriteString(w, "[-:-]")
  171. }
  172. }
  173. func attrToTag(attr discordmd.Attribute) (string, string) {
  174. switch attr {
  175. case discordmd.AttrBold:
  176. return "[::b]", "[::B]"
  177. case discordmd.AttrItalics:
  178. return "[::i]", "[::I]"
  179. case discordmd.AttrUnderline:
  180. return "[::u]", "[::U]"
  181. case discordmd.AttrStrikethrough:
  182. return "[::s]", "[::S]"
  183. case discordmd.AttrMonospace:
  184. return "[::r]", "[::R]"
  185. default:
  186. return "", ""
  187. }
  188. }