markdown.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. package widget
  2. import (
  3. "io"
  4. "net/url"
  5. "strings"
  6. "github.com/yuin/goldmark"
  7. "github.com/yuin/goldmark/ast"
  8. "github.com/yuin/goldmark/renderer"
  9. "fyne.io/fyne/v2"
  10. "fyne.io/fyne/v2/storage"
  11. )
  12. // NewRichTextFromMarkdown configures a RichText widget by parsing the provided markdown content.
  13. //
  14. // Since: 2.1
  15. func NewRichTextFromMarkdown(content string) *RichText {
  16. return NewRichText(parseMarkdown(content)...)
  17. }
  18. // ParseMarkdown allows setting the content of this RichText widget from a markdown string.
  19. // It will replace the content of this widget similarly to SetText, but with the appropriate formatting.
  20. func (t *RichText) ParseMarkdown(content string) {
  21. t.Segments = parseMarkdown(content)
  22. t.Refresh()
  23. }
  24. type markdownRenderer struct {
  25. blockquote bool
  26. heading bool
  27. nextSeg RichTextSegment
  28. parentStack [][]RichTextSegment
  29. segs []RichTextSegment
  30. }
  31. func (m *markdownRenderer) AddOptions(...renderer.Option) {}
  32. func (m *markdownRenderer) Render(_ io.Writer, source []byte, n ast.Node) error {
  33. m.nextSeg = &TextSegment{}
  34. err := ast.Walk(n, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
  35. if !entering {
  36. if n.Kind().String() == "Heading" {
  37. m.segs = append(m.segs, m.nextSeg)
  38. m.heading = false
  39. }
  40. return ast.WalkContinue, m.handleExitNode(n)
  41. }
  42. switch n.Kind().String() {
  43. case "List":
  44. // prepare a new child level
  45. m.parentStack = append(m.parentStack, m.segs)
  46. m.segs = nil
  47. case "ListItem":
  48. // prepare a new item level
  49. m.parentStack = append(m.parentStack, m.segs)
  50. m.segs = nil
  51. case "Heading":
  52. m.heading = true
  53. switch n.(*ast.Heading).Level {
  54. case 1:
  55. m.nextSeg = &TextSegment{
  56. Style: RichTextStyleHeading,
  57. }
  58. case 2:
  59. m.nextSeg = &TextSegment{
  60. Style: RichTextStyleSubHeading,
  61. }
  62. default:
  63. m.nextSeg = &TextSegment{
  64. Style: RichTextStyleParagraph,
  65. }
  66. m.nextSeg.(*TextSegment).Style.TextStyle.Bold = true
  67. }
  68. case "HorizontalRule", "ThematicBreak":
  69. m.segs = append(m.segs, &SeparatorSegment{})
  70. case "Link":
  71. m.nextSeg = makeLink(n.(*ast.Link))
  72. case "Paragraph":
  73. m.nextSeg = &TextSegment{
  74. Style: RichTextStyleInline, // we make it a paragraph at the end if there are no more elements
  75. }
  76. if m.blockquote {
  77. m.nextSeg.(*TextSegment).Style = RichTextStyleBlockquote
  78. }
  79. case "CodeSpan":
  80. m.nextSeg = &TextSegment{
  81. Style: RichTextStyleCodeInline,
  82. }
  83. case "CodeBlock", "FencedCodeBlock":
  84. var data []byte
  85. lines := n.Lines()
  86. for i := 0; i < lines.Len(); i++ {
  87. line := lines.At(i)
  88. data = append(data, line.Value(source)...)
  89. }
  90. if len(data) == 0 {
  91. return ast.WalkContinue, nil
  92. }
  93. if data[len(data)-1] == '\n' {
  94. data = data[:len(data)-1]
  95. }
  96. m.segs = append(m.segs, &TextSegment{
  97. Style: RichTextStyleCodeBlock,
  98. Text: string(data),
  99. })
  100. case "Emph", "Emphasis":
  101. switch n.(*ast.Emphasis).Level {
  102. case 2:
  103. m.nextSeg = &TextSegment{
  104. Style: RichTextStyleStrong,
  105. }
  106. default:
  107. m.nextSeg = &TextSegment{
  108. Style: RichTextStyleEmphasis,
  109. }
  110. }
  111. case "Strong":
  112. m.nextSeg = &TextSegment{
  113. Style: RichTextStyleStrong,
  114. }
  115. case "Text":
  116. ret := addTextToSegment(string(n.Text(source)), m.nextSeg, n)
  117. if ret != 0 {
  118. return ret, nil
  119. }
  120. _, isImage := m.nextSeg.(*ImageSegment)
  121. if !m.heading && !isImage {
  122. m.segs = append(m.segs, m.nextSeg)
  123. }
  124. case "Blockquote":
  125. m.blockquote = true
  126. case "Image":
  127. m.nextSeg = makeImage(n.(*ast.Image)) // remember this for applying title
  128. m.segs = append(m.segs, m.nextSeg)
  129. }
  130. return ast.WalkContinue, nil
  131. })
  132. return err
  133. }
  134. func (m *markdownRenderer) handleExitNode(n ast.Node) error {
  135. if n.Kind().String() == "Blockquote" {
  136. m.blockquote = false
  137. } else if n.Kind().String() == "List" {
  138. listSegs := m.segs
  139. m.segs = m.parentStack[len(m.parentStack)-1]
  140. m.parentStack = m.parentStack[:len(m.parentStack)-1]
  141. marker := n.(*ast.List).Marker
  142. m.segs = append(m.segs, &ListSegment{Items: listSegs, Ordered: marker != '*' && marker != '-' && marker != '+'})
  143. } else if n.Kind().String() == "ListItem" {
  144. itemSegs := m.segs
  145. m.segs = m.parentStack[len(m.parentStack)-1]
  146. m.parentStack = m.parentStack[:len(m.parentStack)-1]
  147. m.segs = append(m.segs, &ParagraphSegment{Texts: itemSegs})
  148. } else if !m.blockquote && !m.heading {
  149. if len(m.segs) > 0 {
  150. if text, ok := m.segs[len(m.segs)-1].(*TextSegment); ok && n.Kind().String() == "Paragraph" {
  151. text.Style.Inline = false
  152. }
  153. }
  154. m.nextSeg = &TextSegment{
  155. Style: RichTextStyleInline,
  156. }
  157. }
  158. return nil
  159. }
  160. func addTextToSegment(text string, s RichTextSegment, node ast.Node) ast.WalkStatus {
  161. trimmed := strings.ReplaceAll(text, "\n", " ") // newline inside paragraph is not newline
  162. if trimmed == "" {
  163. return ast.WalkContinue
  164. }
  165. if t, ok := s.(*TextSegment); ok {
  166. next := node.(*ast.Text).NextSibling()
  167. if next != nil {
  168. if nextText, ok := next.(*ast.Text); ok {
  169. if nextText.Segment.Start > node.(*ast.Text).Segment.Stop { // detect presence of a trailing newline
  170. trimmed = trimmed + " "
  171. }
  172. }
  173. }
  174. t.Text = t.Text + trimmed
  175. }
  176. if link, ok := s.(*HyperlinkSegment); ok {
  177. link.Text = link.Text + trimmed
  178. }
  179. return 0
  180. }
  181. func makeImage(n *ast.Image) *ImageSegment {
  182. dest := string(n.Destination)
  183. u, err := storage.ParseURI(dest)
  184. if err != nil {
  185. u = storage.NewFileURI(dest)
  186. }
  187. return &ImageSegment{Source: u, Title: string(n.Title), Alignment: fyne.TextAlignCenter}
  188. }
  189. func makeLink(n *ast.Link) *HyperlinkSegment {
  190. link, _ := url.Parse(string(n.Destination))
  191. return &HyperlinkSegment{fyne.TextAlignLeading, "", link, nil}
  192. }
  193. func parseMarkdown(content string) []RichTextSegment {
  194. r := &markdownRenderer{}
  195. if content == "" {
  196. return r.segs
  197. }
  198. md := goldmark.New(goldmark.WithRenderer(r))
  199. err := md.Convert([]byte(content), nil)
  200. if err != nil {
  201. fyne.LogError("Failed to parse markdown", err)
  202. }
  203. return r.segs
  204. }