renderer.go 5.6 KB

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