You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

goldmark.go 5.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package markdown
  4. import (
  5. "fmt"
  6. "regexp"
  7. "strings"
  8. "code.gitea.io/gitea/modules/container"
  9. "code.gitea.io/gitea/modules/markup"
  10. "code.gitea.io/gitea/modules/setting"
  11. "github.com/yuin/goldmark/ast"
  12. east "github.com/yuin/goldmark/extension/ast"
  13. "github.com/yuin/goldmark/parser"
  14. "github.com/yuin/goldmark/renderer"
  15. "github.com/yuin/goldmark/renderer/html"
  16. "github.com/yuin/goldmark/text"
  17. "github.com/yuin/goldmark/util"
  18. )
  19. // ASTTransformer is a default transformer of the goldmark tree.
  20. type ASTTransformer struct {
  21. attentionTypes container.Set[string]
  22. }
  23. func NewASTTransformer() *ASTTransformer {
  24. return &ASTTransformer{
  25. attentionTypes: container.SetOf("note", "tip", "important", "warning", "caution"),
  26. }
  27. }
  28. func (g *ASTTransformer) applyElementDir(n ast.Node) {
  29. if markup.DefaultProcessorHelper.ElementDir != "" {
  30. n.SetAttributeString("dir", []byte(markup.DefaultProcessorHelper.ElementDir))
  31. }
  32. }
  33. // Transform transforms the given AST tree.
  34. func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
  35. firstChild := node.FirstChild()
  36. tocMode := ""
  37. ctx := pc.Get(renderContextKey).(*markup.RenderContext)
  38. rc := pc.Get(renderConfigKey).(*RenderConfig)
  39. tocList := make([]markup.Header, 0, 20)
  40. if rc.yamlNode != nil {
  41. metaNode := rc.toMetaNode()
  42. if metaNode != nil {
  43. node.InsertBefore(node, firstChild, metaNode)
  44. }
  45. tocMode = rc.TOC
  46. }
  47. _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
  48. if !entering {
  49. return ast.WalkContinue, nil
  50. }
  51. switch v := n.(type) {
  52. case *ast.Heading:
  53. g.transformHeading(ctx, v, reader, &tocList)
  54. case *ast.Paragraph:
  55. g.applyElementDir(v)
  56. case *ast.Image:
  57. g.transformImage(ctx, v)
  58. case *ast.Link:
  59. g.transformLink(ctx, v)
  60. case *ast.List:
  61. g.transformList(ctx, v, rc)
  62. case *ast.Text:
  63. if v.SoftLineBreak() && !v.HardLineBreak() {
  64. if ctx.Metas["mode"] != "document" {
  65. v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments)
  66. } else {
  67. v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments)
  68. }
  69. }
  70. case *ast.CodeSpan:
  71. g.transformCodeSpan(ctx, v, reader)
  72. case *ast.Blockquote:
  73. return g.transformBlockquote(v, reader)
  74. }
  75. return ast.WalkContinue, nil
  76. })
  77. showTocInMain := tocMode == "true" /* old behavior, in main view */ || tocMode == "main"
  78. showTocInSidebar := !showTocInMain && tocMode != "false" // not hidden, not main, then show it in sidebar
  79. if len(tocList) > 0 && (showTocInMain || showTocInSidebar) {
  80. if showTocInMain {
  81. tocNode := createTOCNode(tocList, rc.Lang, nil)
  82. node.InsertBefore(node, firstChild, tocNode)
  83. } else {
  84. tocNode := createTOCNode(tocList, rc.Lang, map[string]string{"open": "open"})
  85. ctx.SidebarTocNode = tocNode
  86. }
  87. }
  88. if len(rc.Lang) > 0 {
  89. node.SetAttributeString("lang", []byte(rc.Lang))
  90. }
  91. }
  92. // NewHTMLRenderer creates a HTMLRenderer to render
  93. // in the gitea form.
  94. func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
  95. r := &HTMLRenderer{
  96. Config: html.NewConfig(),
  97. reValidName: regexp.MustCompile("^[a-z ]+$"),
  98. }
  99. for _, opt := range opts {
  100. opt.SetHTMLOption(&r.Config)
  101. }
  102. return r
  103. }
  104. // HTMLRenderer is a renderer.NodeRenderer implementation that
  105. // renders gitea specific features.
  106. type HTMLRenderer struct {
  107. html.Config
  108. reValidName *regexp.Regexp
  109. }
  110. // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
  111. func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
  112. reg.Register(ast.KindDocument, r.renderDocument)
  113. reg.Register(KindDetails, r.renderDetails)
  114. reg.Register(KindSummary, r.renderSummary)
  115. reg.Register(KindIcon, r.renderIcon)
  116. reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
  117. reg.Register(KindAttention, r.renderAttention)
  118. reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
  119. reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
  120. }
  121. func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
  122. n := node.(*ast.Document)
  123. if val, has := n.AttributeString("lang"); has {
  124. var err error
  125. if entering {
  126. _, err = w.WriteString("<div")
  127. if err == nil {
  128. _, err = w.WriteString(fmt.Sprintf(` lang=%q`, val))
  129. }
  130. if err == nil {
  131. _, err = w.WriteRune('>')
  132. }
  133. } else {
  134. _, err = w.WriteString("</div>")
  135. }
  136. if err != nil {
  137. return ast.WalkStop, err
  138. }
  139. }
  140. return ast.WalkContinue, nil
  141. }
  142. func (r *HTMLRenderer) renderDetails(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
  143. var err error
  144. if entering {
  145. if _, err = w.WriteString("<details"); err != nil {
  146. return ast.WalkStop, err
  147. }
  148. html.RenderAttributes(w, node, nil)
  149. _, err = w.WriteString(">")
  150. } else {
  151. _, err = w.WriteString("</details>")
  152. }
  153. if err != nil {
  154. return ast.WalkStop, err
  155. }
  156. return ast.WalkContinue, nil
  157. }
  158. func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
  159. var err error
  160. if entering {
  161. _, err = w.WriteString("<summary>")
  162. } else {
  163. _, err = w.WriteString("</summary>")
  164. }
  165. if err != nil {
  166. return ast.WalkStop, err
  167. }
  168. return ast.WalkContinue, nil
  169. }
  170. func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
  171. if !entering {
  172. return ast.WalkContinue, nil
  173. }
  174. n := node.(*Icon)
  175. name := strings.TrimSpace(strings.ToLower(string(n.Name)))
  176. if len(name) == 0 {
  177. // skip this
  178. return ast.WalkContinue, nil
  179. }
  180. if !r.reValidName.MatchString(name) {
  181. // skip this
  182. return ast.WalkContinue, nil
  183. }
  184. var err error
  185. _, err = w.WriteString(fmt.Sprintf(`<i class="icon %s"></i>`, name))
  186. if err != nil {
  187. return ast.WalkStop, err
  188. }
  189. return ast.WalkContinue, nil
  190. }