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

goldmark.go 5.5 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package markdown
  5. import (
  6. "bytes"
  7. "fmt"
  8. "strings"
  9. "code.gitea.io/gitea/modules/markup"
  10. "code.gitea.io/gitea/modules/markup/common"
  11. giteautil "code.gitea.io/gitea/modules/util"
  12. "github.com/yuin/goldmark/ast"
  13. east "github.com/yuin/goldmark/extension/ast"
  14. "github.com/yuin/goldmark/parser"
  15. "github.com/yuin/goldmark/renderer"
  16. "github.com/yuin/goldmark/renderer/html"
  17. "github.com/yuin/goldmark/text"
  18. "github.com/yuin/goldmark/util"
  19. )
  20. var byteMailto = []byte("mailto:")
  21. // GiteaASTTransformer is a default transformer of the goldmark tree.
  22. type GiteaASTTransformer struct{}
  23. // Transform transforms the given AST tree.
  24. func (g *GiteaASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
  25. _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
  26. if !entering {
  27. return ast.WalkContinue, nil
  28. }
  29. switch v := n.(type) {
  30. case *ast.Image:
  31. // Images need two things:
  32. //
  33. // 1. Their src needs to munged to be a real value
  34. // 2. If they're not wrapped with a link they need a link wrapper
  35. // Check if the destination is a real link
  36. link := v.Destination
  37. if len(link) > 0 && !markup.IsLink(link) {
  38. prefix := pc.Get(urlPrefixKey).(string)
  39. if pc.Get(isWikiKey).(bool) {
  40. prefix = giteautil.URLJoin(prefix, "wiki", "raw")
  41. }
  42. prefix = strings.Replace(prefix, "/src/", "/media/", 1)
  43. lnk := string(link)
  44. lnk = giteautil.URLJoin(prefix, lnk)
  45. link = []byte(lnk)
  46. }
  47. v.Destination = link
  48. parent := n.Parent()
  49. // Create a link around image only if parent is not already a link
  50. if _, ok := parent.(*ast.Link); !ok && parent != nil {
  51. wrap := ast.NewLink()
  52. wrap.Destination = link
  53. wrap.Title = v.Title
  54. parent.ReplaceChild(parent, n, wrap)
  55. wrap.AppendChild(wrap, n)
  56. }
  57. case *ast.Link:
  58. // Links need their href to munged to be a real value
  59. link := v.Destination
  60. if len(link) > 0 && !markup.IsLink(link) &&
  61. link[0] != '#' && !bytes.HasPrefix(link, byteMailto) {
  62. // special case: this is not a link, a hash link or a mailto:, so it's a
  63. // relative URL
  64. lnk := string(link)
  65. if pc.Get(isWikiKey).(bool) {
  66. lnk = giteautil.URLJoin("wiki", lnk)
  67. }
  68. link = []byte(giteautil.URLJoin(pc.Get(urlPrefixKey).(string), lnk))
  69. }
  70. if len(link) > 0 && link[0] == '#' {
  71. link = []byte("#user-content-" + string(link)[1:])
  72. }
  73. v.Destination = link
  74. case *ast.List:
  75. if v.HasChildren() && v.FirstChild().HasChildren() && v.FirstChild().FirstChild().HasChildren() {
  76. if _, ok := v.FirstChild().FirstChild().FirstChild().(*east.TaskCheckBox); ok {
  77. v.SetAttributeString("class", []byte("task-list"))
  78. }
  79. }
  80. }
  81. return ast.WalkContinue, nil
  82. })
  83. }
  84. type prefixedIDs struct {
  85. values map[string]bool
  86. }
  87. // Generate generates a new element id.
  88. func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte {
  89. dft := []byte("id")
  90. if kind == ast.KindHeading {
  91. dft = []byte("heading")
  92. }
  93. return p.GenerateWithDefault(value, dft)
  94. }
  95. // Generate generates a new element id.
  96. func (p *prefixedIDs) GenerateWithDefault(value []byte, dft []byte) []byte {
  97. result := common.CleanValue(value)
  98. if len(result) == 0 {
  99. result = dft
  100. }
  101. if !bytes.HasPrefix(result, []byte("user-content-")) {
  102. result = append([]byte("user-content-"), result...)
  103. }
  104. if _, ok := p.values[util.BytesToReadOnlyString(result)]; !ok {
  105. p.values[util.BytesToReadOnlyString(result)] = true
  106. return result
  107. }
  108. for i := 1; ; i++ {
  109. newResult := fmt.Sprintf("%s-%d", result, i)
  110. if _, ok := p.values[newResult]; !ok {
  111. p.values[newResult] = true
  112. return []byte(newResult)
  113. }
  114. }
  115. }
  116. // Put puts a given element id to the used ids table.
  117. func (p *prefixedIDs) Put(value []byte) {
  118. p.values[util.BytesToReadOnlyString(value)] = true
  119. }
  120. func newPrefixedIDs() *prefixedIDs {
  121. return &prefixedIDs{
  122. values: map[string]bool{},
  123. }
  124. }
  125. // NewTaskCheckBoxHTMLRenderer creates a TaskCheckBoxHTMLRenderer to render tasklists
  126. // in the gitea form.
  127. func NewTaskCheckBoxHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
  128. r := &TaskCheckBoxHTMLRenderer{
  129. Config: html.NewConfig(),
  130. }
  131. for _, opt := range opts {
  132. opt.SetHTMLOption(&r.Config)
  133. }
  134. return r
  135. }
  136. // TaskCheckBoxHTMLRenderer is a renderer.NodeRenderer implementation that
  137. // renders checkboxes in list items.
  138. // Overrides the default goldmark one to present the gitea format
  139. type TaskCheckBoxHTMLRenderer struct {
  140. html.Config
  141. }
  142. // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
  143. func (r *TaskCheckBoxHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
  144. reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
  145. }
  146. func (r *TaskCheckBoxHTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
  147. if !entering {
  148. return ast.WalkContinue, nil
  149. }
  150. n := node.(*east.TaskCheckBox)
  151. end := ">"
  152. if r.XHTML {
  153. end = " />"
  154. }
  155. var err error
  156. if n.IsChecked {
  157. _, err = w.WriteString(`<span class="ui fitted disabled checkbox"><input type="checkbox" disabled="disabled"` + end + `<label` + end + `</span>`)
  158. } else {
  159. _, err = w.WriteString(`<span class="ui checked fitted disabled checkbox"><input type="checkbox" checked="" disabled="disabled"` + end + `<label` + end + `</span>`)
  160. }
  161. if err != nil {
  162. return ast.WalkStop, err
  163. }
  164. return ast.WalkContinue, nil
  165. }