markdown.go 4.9 KB

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