renderer.go 5.3 KB

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