markdown.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. package markup
  2. import (
  3. "bytes"
  4. "fmt"
  5. "html"
  6. "net/url"
  7. "path"
  8. "path/filepath"
  9. "strings"
  10. "github.com/yuin/goldmark"
  11. "github.com/yuin/goldmark/ast"
  12. "github.com/yuin/goldmark/extension"
  13. "github.com/yuin/goldmark/parser"
  14. "github.com/yuin/goldmark/renderer"
  15. goldmarkhtml "github.com/yuin/goldmark/renderer/html"
  16. "github.com/yuin/goldmark/text"
  17. "github.com/yuin/goldmark/util"
  18. log "unknwon.dev/clog/v2"
  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 = lazyregexp.New(`^(?: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(joinURLPath(t.urlPrefix, string(dest)))
  52. }
  53. }
  54. return ast.WalkContinue, nil
  55. })
  56. }
  57. // joinURLPath joins a base URL prefix with a relative path. It uses net/url for
  58. // absolute URL prefixes to preserve the scheme and host, and path.Join for
  59. // path-only prefixes.
  60. func joinURLPath(prefix, relPath string) string {
  61. u, err := url.Parse(prefix)
  62. if err != nil || u.Scheme == "" {
  63. return path.Join(prefix, relPath)
  64. }
  65. u.Path = path.Join(u.Path, relPath)
  66. return u.String()
  67. }
  68. type gogsRenderer struct {
  69. urlPrefix string
  70. }
  71. func (r *gogsRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
  72. reg.Register(ast.KindAutoLink, r.renderAutoLink)
  73. }
  74. func (r *gogsRenderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
  75. n := node.(*ast.AutoLink)
  76. if !entering {
  77. return ast.WalkContinue, nil
  78. }
  79. if n.AutoLinkType != ast.AutoLinkURL {
  80. url := n.URL(source)
  81. escaped := html.EscapeString(string(url))
  82. _, _ = fmt.Fprintf(w, `<a href="mailto:%s">%s</a>`, escaped, escaped)
  83. return ast.WalkContinue, nil
  84. }
  85. link := n.URL(source)
  86. if bytes.HasPrefix(link, []byte(conf.Server.ExternalURL)) {
  87. m := CommitPattern.Find(link)
  88. if m != nil {
  89. m = bytes.TrimSpace(m)
  90. i := bytes.Index(m, []byte("commit/"))
  91. j := bytes.Index(m, []byte("#"))
  92. if j == -1 {
  93. j = len(m)
  94. }
  95. escapedURL := html.EscapeString(string(m))
  96. _, _ = fmt.Fprintf(w, ` <code><a href="%s">%s</a></code>`, escapedURL, tool.ShortSHA1(string(m[i+7:j])))
  97. return ast.WalkContinue, nil
  98. }
  99. m = IssueFullPattern.Find(link)
  100. if m != nil {
  101. m = bytes.TrimSpace(m)
  102. i := bytes.Index(m, []byte("issues/"))
  103. j := bytes.Index(m, []byte("#"))
  104. if j == -1 {
  105. j = len(m)
  106. }
  107. index := string(m[i+7 : j])
  108. escapedURL := html.EscapeString(string(m))
  109. fullRepoURL := conf.Server.ExternalURL + strings.TrimPrefix(r.urlPrefix, "/")
  110. var href string
  111. if strings.HasPrefix(string(m), fullRepoURL) {
  112. href = fmt.Sprintf(`<a href="%s">#%s</a>`, escapedURL, html.EscapeString(index))
  113. } else {
  114. repo := html.EscapeString(string(m[len(conf.Server.ExternalURL) : i-1]))
  115. href = fmt.Sprintf(`<a href="%s">%s#%s</a>`, escapedURL, repo, html.EscapeString(index))
  116. }
  117. _, _ = w.WriteString(href)
  118. return ast.WalkContinue, nil
  119. }
  120. }
  121. escapedLink := html.EscapeString(string(link))
  122. _, _ = fmt.Fprintf(w, `<a href="%s">%s</a>`, escapedLink, escapedLink)
  123. return ast.WalkContinue, nil
  124. }
  125. // RawMarkdown renders content in Markdown syntax to HTML without handling special links.
  126. func RawMarkdown(body []byte, urlPrefix string) []byte {
  127. extensions := []goldmark.Extender{
  128. extension.Table,
  129. extension.Strikethrough,
  130. extension.TaskList,
  131. extension.NewLinkify(extension.WithLinkifyURLRegexp(linkifyURLRegexp.Regexp())),
  132. }
  133. if conf.Smartypants.Enabled {
  134. extensions = append(extensions, extension.Typographer)
  135. }
  136. rendererOpts := []renderer.Option{
  137. renderer.WithNodeRenderers(
  138. util.Prioritized(&gogsRenderer{urlPrefix: urlPrefix}, 0),
  139. ),
  140. }
  141. if conf.Markdown.EnableHardLineBreak {
  142. rendererOpts = append(rendererOpts, goldmarkhtml.WithHardWraps())
  143. }
  144. md := goldmark.New(
  145. goldmark.WithExtensions(extensions...),
  146. goldmark.WithParserOptions(
  147. parser.WithASTTransformers(
  148. util.Prioritized(&linkTransformer{urlPrefix: urlPrefix}, 0),
  149. ),
  150. ),
  151. goldmark.WithRendererOptions(rendererOpts...),
  152. )
  153. var buf bytes.Buffer
  154. if err := md.Convert(body, &buf); err != nil {
  155. log.Error("Failed to convert Markdown: %v", err)
  156. return []byte(html.EscapeString(string(body)))
  157. }
  158. return buf.Bytes()
  159. }
  160. // Markdown takes a string or []byte and renders to HTML in Markdown syntax with special links.
  161. func Markdown(input any, urlPrefix string, metas map[string]string) []byte {
  162. return Render(TypeMarkdown, input, urlPrefix, metas)
  163. }