markdown.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. package markup
  2. import (
  3. "bytes"
  4. "fmt"
  5. "html"
  6. "log"
  7. "path"
  8. "path/filepath"
  9. "regexp"
  10. "strings"
  11. "github.com/yuin/goldmark"
  12. "github.com/yuin/goldmark/ast"
  13. "github.com/yuin/goldmark/extension"
  14. "github.com/yuin/goldmark/parser"
  15. "github.com/yuin/goldmark/renderer"
  16. goldmarkhtml "github.com/yuin/goldmark/renderer/html"
  17. "github.com/yuin/goldmark/text"
  18. "github.com/yuin/goldmark/util"
  19. "gogs.io/gogs/internal/conf"
  20. "gogs.io/gogs/internal/lazyregexp"
  21. "gogs.io/gogs/internal/tool"
  22. )
  23. // IsMarkdownFile reports whether name looks like a Markdown file based on its extension.
  24. func IsMarkdownFile(name string) bool {
  25. extension := strings.ToLower(filepath.Ext(name))
  26. for _, ext := range conf.Markdown.FileExtensions {
  27. if strings.ToLower(ext) == extension {
  28. return true
  29. }
  30. }
  31. return false
  32. }
  33. var (
  34. validLinksPattern = lazyregexp.New(`^[a-z][\w-]+://|^mailto:`)
  35. linkifyURLRegexp = regexp.MustCompile(`^(?:http|https|ftp)://[-a-zA-Z0-9@:%._+~#=]{1,256}(?:\.[a-z]+)?(?::\d+)?(?:[/#?][-a-zA-Z0-9@:%_+.~#$!?&/=();,'\^{}\[\]` + "`" + `]*)?`)
  36. )
  37. func isLink(link []byte) bool {
  38. return validLinksPattern.Match(link)
  39. }
  40. type linkTransformer struct {
  41. urlPrefix string
  42. }
  43. func (t *linkTransformer) Transform(node *ast.Document, reader text.Reader, _ parser.Context) {
  44. _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
  45. if !entering {
  46. return ast.WalkContinue, nil
  47. }
  48. if link, ok := n.(*ast.Link); ok {
  49. dest := link.Destination
  50. if len(dest) > 0 && !isLink(dest) && dest[0] != '#' {
  51. link.Destination = []byte(path.Join(t.urlPrefix, string(dest)))
  52. }
  53. }
  54. return ast.WalkContinue, nil
  55. })
  56. }
  57. type gogsRenderer struct {
  58. urlPrefix string
  59. }
  60. func (r *gogsRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
  61. reg.Register(ast.KindAutoLink, r.renderAutoLink)
  62. }
  63. func (r *gogsRenderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
  64. n := node.(*ast.AutoLink)
  65. if !entering {
  66. return ast.WalkContinue, nil
  67. }
  68. if n.AutoLinkType != ast.AutoLinkURL {
  69. url := n.URL(source)
  70. escaped := html.EscapeString(string(url))
  71. _, _ = fmt.Fprintf(w, `<a href="mailto:%s">%s</a>`, escaped, escaped)
  72. return ast.WalkContinue, nil
  73. }
  74. link := n.URL(source)
  75. if bytes.HasPrefix(link, []byte(conf.Server.ExternalURL)) {
  76. m := CommitPattern.Find(link)
  77. if m != nil {
  78. m = bytes.TrimSpace(m)
  79. i := bytes.Index(m, []byte("commit/"))
  80. j := bytes.Index(m, []byte("#"))
  81. if j == -1 {
  82. j = len(m)
  83. }
  84. escapedURL := html.EscapeString(string(m))
  85. _, _ = fmt.Fprintf(w, ` <code><a href="%s">%s</a></code>`, escapedURL, tool.ShortSHA1(string(m[i+7:j])))
  86. return ast.WalkContinue, nil
  87. }
  88. m = IssueFullPattern.Find(link)
  89. if m != nil {
  90. m = bytes.TrimSpace(m)
  91. i := bytes.Index(m, []byte("issues/"))
  92. j := bytes.Index(m, []byte("#"))
  93. if j == -1 {
  94. j = len(m)
  95. }
  96. index := string(m[i+7 : j])
  97. escapedURL := html.EscapeString(string(m))
  98. fullRepoURL := conf.Server.ExternalURL + strings.TrimPrefix(r.urlPrefix, "/")
  99. var href string
  100. if strings.HasPrefix(string(m), fullRepoURL) {
  101. href = fmt.Sprintf(`<a href="%s">#%s</a>`, escapedURL, html.EscapeString(index))
  102. } else {
  103. repo := html.EscapeString(string(m[len(conf.Server.ExternalURL) : i-1]))
  104. href = fmt.Sprintf(`<a href="%s">%s#%s</a>`, escapedURL, repo, html.EscapeString(index))
  105. }
  106. _, _ = w.WriteString(href)
  107. return ast.WalkContinue, nil
  108. }
  109. }
  110. escapedLink := html.EscapeString(string(link))
  111. _, _ = fmt.Fprintf(w, `<a href="%s">%s</a>`, escapedLink, escapedLink)
  112. return ast.WalkContinue, nil
  113. }
  114. // RawMarkdown renders content in Markdown syntax to HTML without handling special links.
  115. func RawMarkdown(body []byte, urlPrefix string) []byte {
  116. extensions := []goldmark.Extender{
  117. extension.Table,
  118. extension.Strikethrough,
  119. extension.TaskList,
  120. extension.NewLinkify(extension.WithLinkifyURLRegexp(linkifyURLRegexp)),
  121. }
  122. if conf.Smartypants.Enabled {
  123. extensions = append(extensions, extension.Typographer)
  124. }
  125. rendererOpts := []renderer.Option{
  126. goldmarkhtml.WithUnsafe(),
  127. renderer.WithNodeRenderers(
  128. util.Prioritized(&gogsRenderer{urlPrefix: urlPrefix}, 0),
  129. ),
  130. }
  131. if conf.Markdown.EnableHardLineBreak {
  132. rendererOpts = append(rendererOpts, goldmarkhtml.WithHardWraps())
  133. }
  134. md := goldmark.New(
  135. goldmark.WithExtensions(extensions...),
  136. goldmark.WithParserOptions(
  137. parser.WithASTTransformers(
  138. util.Prioritized(&linkTransformer{urlPrefix: urlPrefix}, 0),
  139. ),
  140. ),
  141. goldmark.WithRendererOptions(rendererOpts...),
  142. )
  143. var buf bytes.Buffer
  144. if err := md.Convert(body, &buf); err != nil {
  145. log.Printf("markup: failed to convert Markdown: %v", err)
  146. return nil
  147. }
  148. return buf.Bytes()
  149. }
  150. // Markdown takes a string or []byte and renders to HTML in Markdown syntax with special links.
  151. func Markdown(input any, urlPrefix string, metas map[string]string) []byte {
  152. return Render(TypeMarkdown, input, urlPrefix, metas)
  153. }