renderer.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. package markdown
  2. import (
  3. "strconv"
  4. "strings"
  5. "github.com/ayn2op/discordo/internal/config"
  6. "github.com/ayn2op/discordo/internal/ui"
  7. "github.com/ayn2op/tview"
  8. "github.com/diamondburned/ningen/v3/discordmd"
  9. "github.com/gdamore/tcell/v3"
  10. "github.com/yuin/goldmark/ast"
  11. )
  12. type Renderer struct {
  13. theme config.MessagesListTheme
  14. listIx *int
  15. listNested int
  16. }
  17. func NewRenderer(theme config.MessagesListTheme) *Renderer {
  18. return &Renderer{theme: theme}
  19. }
  20. func (r *Renderer) RenderLines(source []byte, node ast.Node, base tcell.Style) []tview.Line {
  21. r.listIx = nil
  22. r.listNested = 0
  23. builder := tview.NewLineBuilder()
  24. styleStack := []tcell.Style{base}
  25. currentStyle := func() tcell.Style {
  26. return styleStack[len(styleStack)-1]
  27. }
  28. pushStyle := func(style tcell.Style) {
  29. styleStack = append(styleStack, style)
  30. }
  31. popStyle := func() {
  32. if len(styleStack) > 1 {
  33. styleStack = styleStack[:len(styleStack)-1]
  34. }
  35. }
  36. _ = ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
  37. switch node := node.(type) {
  38. case *ast.Document:
  39. // noop
  40. case *ast.Heading:
  41. if entering {
  42. builder.Write(strings.Repeat("#", node.Level)+" ", currentStyle())
  43. } else {
  44. builder.NewLine()
  45. }
  46. case *ast.Text:
  47. if entering {
  48. builder.Write(string(node.Segment.Value(source)), currentStyle())
  49. switch {
  50. case node.HardLineBreak():
  51. builder.NewLine()
  52. builder.NewLine()
  53. case node.SoftLineBreak():
  54. builder.NewLine()
  55. }
  56. }
  57. case *ast.FencedCodeBlock:
  58. if entering {
  59. builder.NewLine()
  60. if language := node.Language(source); language != nil {
  61. builder.Write("|=> "+string(language), currentStyle())
  62. builder.NewLine()
  63. }
  64. lines := node.Lines()
  65. for i := range lines.Len() {
  66. line := lines.At(i)
  67. builder.Write("| "+string(line.Value(source)), currentStyle())
  68. }
  69. }
  70. case *ast.AutoLink:
  71. if entering {
  72. style := ui.MergeStyle(currentStyle(), r.theme.URLStyle.Style)
  73. builder.Write(string(node.URL(source)), style)
  74. }
  75. case *ast.Link:
  76. if entering {
  77. pushStyle(ui.MergeStyle(currentStyle(), r.theme.URLStyle.Style))
  78. } else {
  79. popStyle()
  80. }
  81. case *ast.List:
  82. if node.IsOrdered() {
  83. start := node.Start
  84. r.listIx = &start
  85. } else {
  86. r.listIx = nil
  87. }
  88. if entering {
  89. builder.NewLine()
  90. r.listNested++
  91. } else {
  92. r.listNested--
  93. }
  94. case *ast.ListItem:
  95. if entering {
  96. builder.Write(strings.Repeat(" ", r.listNested-1), currentStyle())
  97. if r.listIx != nil {
  98. builder.Write(strconv.Itoa(*r.listIx)+". ", currentStyle())
  99. *r.listIx++
  100. } else {
  101. builder.Write("- ", currentStyle())
  102. }
  103. } else {
  104. builder.NewLine()
  105. }
  106. case *discordmd.Inline:
  107. if entering {
  108. pushStyle(applyInlineAttr(currentStyle(), node.Attr))
  109. } else {
  110. popStyle()
  111. }
  112. case *discordmd.Mention:
  113. if entering {
  114. style := ui.MergeStyle(currentStyle(), r.theme.MentionStyle.Style)
  115. style = style.Bold(true)
  116. builder.Write(mentionText(node), style)
  117. }
  118. case *discordmd.Emoji:
  119. if entering {
  120. style := ui.MergeStyle(currentStyle(), r.theme.EmojiStyle.Style)
  121. builder.Write(":"+node.Name+":", style)
  122. }
  123. }
  124. return ast.WalkContinue, nil
  125. })
  126. return builder.Finish()
  127. }
  128. func mentionText(node *discordmd.Mention) string {
  129. switch {
  130. case node.Channel != nil:
  131. return "#" + node.Channel.Name
  132. case node.GuildUser != nil:
  133. name := node.GuildUser.DisplayOrUsername()
  134. if member := node.GuildUser.Member; member != nil && member.Nick != "" {
  135. name = member.Nick
  136. }
  137. return "@" + name
  138. case node.GuildRole != nil:
  139. return "@" + node.GuildRole.Name
  140. default:
  141. return ""
  142. }
  143. }
  144. func applyInlineAttr(style tcell.Style, attr discordmd.Attribute) tcell.Style {
  145. switch attr {
  146. case discordmd.AttrBold:
  147. return style.Bold(true)
  148. case discordmd.AttrItalics:
  149. return style.Italic(true)
  150. case discordmd.AttrUnderline:
  151. // tcell v3 in this project does not expose underline attrs.
  152. return style
  153. case discordmd.AttrStrikethrough:
  154. return style.StrikeThrough(true)
  155. case discordmd.AttrMonospace:
  156. return style.Reverse(true)
  157. }
  158. return style
  159. }