renderer.go 4.6 KB

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