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.

354 lines
9.4 KiB

10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
  1. // Copyright 2014 The Gogs 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 base
  5. import (
  6. "bytes"
  7. "fmt"
  8. "io"
  9. "net/http"
  10. "path"
  11. "path/filepath"
  12. "regexp"
  13. "strings"
  14. "github.com/Unknwon/com"
  15. "github.com/russross/blackfriday"
  16. "golang.org/x/net/html"
  17. "github.com/gogits/gogs/modules/setting"
  18. )
  19. // TODO: put this into 'markdown' module.
  20. func isletter(c byte) bool {
  21. return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
  22. }
  23. func isalnum(c byte) bool {
  24. return (c >= '0' && c <= '9') || isletter(c)
  25. }
  26. var validLinksPattern = regexp.MustCompile(`^[a-z][\w-]+://`)
  27. func isLink(link []byte) bool {
  28. return validLinksPattern.Match(link)
  29. }
  30. func IsMarkdownFile(name string) bool {
  31. name = strings.ToLower(name)
  32. switch filepath.Ext(name) {
  33. case ".md", ".markdown", ".mdown", ".mkd":
  34. return true
  35. }
  36. return false
  37. }
  38. func IsTextFile(data []byte) (string, bool) {
  39. contentType := http.DetectContentType(data)
  40. if strings.Index(contentType, "text/") != -1 {
  41. return contentType, true
  42. }
  43. return contentType, false
  44. }
  45. func IsImageFile(data []byte) (string, bool) {
  46. contentType := http.DetectContentType(data)
  47. if strings.Index(contentType, "image/") != -1 {
  48. return contentType, true
  49. }
  50. return contentType, false
  51. }
  52. // IsReadmeFile returns true if given file name suppose to be a README file.
  53. func IsReadmeFile(name string) bool {
  54. name = strings.ToLower(name)
  55. if len(name) < 6 {
  56. return false
  57. } else if len(name) == 6 {
  58. if name == "readme" {
  59. return true
  60. }
  61. return false
  62. }
  63. if name[:7] == "readme." {
  64. return true
  65. }
  66. return false
  67. }
  68. var (
  69. MentionPattern = regexp.MustCompile(`(\s|^)@[0-9a-zA-Z_\.]+`)
  70. commitPattern = regexp.MustCompile(`(\s|^)https?.*commit/[0-9a-zA-Z]+(#+[0-9a-zA-Z-]*)?`)
  71. issueFullPattern = regexp.MustCompile(`(\s|^)https?.*issues/[0-9]+(#+[0-9a-zA-Z-]*)?`)
  72. issueIndexPattern = regexp.MustCompile(`( |^|\()#[0-9]+\b`)
  73. sha1CurrentPattern = regexp.MustCompile(`\b[0-9a-f]{40}\b`)
  74. )
  75. type CustomRender struct {
  76. blackfriday.Renderer
  77. urlPrefix string
  78. }
  79. func (r *CustomRender) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
  80. if len(link) > 0 && !isLink(link) {
  81. if link[0] == '#' {
  82. // link = append([]byte(options.urlPrefix), link...)
  83. } else {
  84. link = []byte(path.Join(r.urlPrefix, string(link)))
  85. }
  86. }
  87. r.Renderer.Link(out, link, title, content)
  88. }
  89. func (r *CustomRender) AutoLink(out *bytes.Buffer, link []byte, kind int) {
  90. if kind != 1 {
  91. r.Renderer.AutoLink(out, link, kind)
  92. return
  93. }
  94. // This method could only possibly serve one link at a time, no need to find all.
  95. m := commitPattern.Find(link)
  96. if m != nil {
  97. m = bytes.TrimSpace(m)
  98. i := strings.Index(string(m), "commit/")
  99. j := strings.Index(string(m), "#")
  100. if j == -1 {
  101. j = len(m)
  102. }
  103. out.WriteString(fmt.Sprintf(` <code><a href="%s">%s</a></code>`, m, ShortSha(string(m[i+7:j]))))
  104. return
  105. }
  106. m = issueFullPattern.Find(link)
  107. if m != nil {
  108. m = bytes.TrimSpace(m)
  109. i := strings.Index(string(m), "issues/")
  110. j := strings.Index(string(m), "#")
  111. if j == -1 {
  112. j = len(m)
  113. }
  114. out.WriteString(fmt.Sprintf(` <a href="%s">#%s</a>`, m, ShortSha(string(m[i+7:j]))))
  115. return
  116. }
  117. r.Renderer.AutoLink(out, link, kind)
  118. }
  119. func (options *CustomRender) ListItem(out *bytes.Buffer, text []byte, flags int) {
  120. switch {
  121. case bytes.HasPrefix(text, []byte("[ ] ")):
  122. text = append([]byte(`<input type="checkbox" disabled="" />`), text[3:]...)
  123. case bytes.HasPrefix(text, []byte("[x] ")):
  124. text = append([]byte(`<input type="checkbox" disabled="" checked="" />`), text[3:]...)
  125. }
  126. options.Renderer.ListItem(out, text, flags)
  127. }
  128. var (
  129. svgSuffix = []byte(".svg")
  130. svgSuffixWithMark = []byte(".svg?")
  131. spaceBytes = []byte(" ")
  132. spaceEncodedBytes = []byte("%20")
  133. )
  134. func (r *CustomRender) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
  135. prefix := strings.Replace(r.urlPrefix, "/src/", "/raw/", 1)
  136. if len(link) > 0 {
  137. if isLink(link) {
  138. // External link with .svg suffix usually means CI status.
  139. if bytes.HasSuffix(link, svgSuffix) || bytes.Contains(link, svgSuffixWithMark) {
  140. r.Renderer.Image(out, link, title, alt)
  141. return
  142. }
  143. } else {
  144. if link[0] != '/' {
  145. prefix += "/"
  146. }
  147. link = bytes.Replace([]byte((prefix + string(link))), spaceBytes, spaceEncodedBytes, -1)
  148. fmt.Println(333, string(link))
  149. }
  150. }
  151. out.WriteString(`<a href="`)
  152. out.Write(link)
  153. out.WriteString(`">`)
  154. r.Renderer.Image(out, link, title, alt)
  155. out.WriteString("</a>")
  156. }
  157. func cutoutVerbosePrefix(prefix string) string {
  158. count := 0
  159. for i := 0; i < len(prefix); i++ {
  160. if prefix[i] == '/' {
  161. count++
  162. }
  163. if count >= 3+setting.AppSubUrlDepth {
  164. return prefix[:i]
  165. }
  166. }
  167. return prefix
  168. }
  169. func RenderIssueIndexPattern(rawBytes []byte, urlPrefix string, metas map[string]string) []byte {
  170. urlPrefix = cutoutVerbosePrefix(urlPrefix)
  171. ms := issueIndexPattern.FindAll(rawBytes, -1)
  172. for _, m := range ms {
  173. var space string
  174. m2 := m
  175. if m2[0] != '#' {
  176. space = string(m2[0])
  177. m2 = m2[1:]
  178. }
  179. if metas == nil {
  180. rawBytes = bytes.Replace(rawBytes, m, []byte(fmt.Sprintf(`%s<a href="%s/issues/%s">%s</a>`,
  181. space, urlPrefix, m2[1:], m2)), 1)
  182. } else {
  183. // Support for external issue tracker
  184. metas["index"] = string(m2[1:])
  185. rawBytes = bytes.Replace(rawBytes, m, []byte(fmt.Sprintf(`%s<a href="%s">%s</a>`,
  186. space, com.Expand(metas["format"], metas), m2)), 1)
  187. }
  188. }
  189. return rawBytes
  190. }
  191. func RenderSpecialLink(rawBytes []byte, urlPrefix string, metas map[string]string) []byte {
  192. ms := MentionPattern.FindAll(rawBytes, -1)
  193. for _, m := range ms {
  194. m = bytes.TrimSpace(m)
  195. rawBytes = bytes.Replace(rawBytes, m,
  196. []byte(fmt.Sprintf(`<a href="%s/%s">%s</a>`, setting.AppSubUrl, m[1:], m)), -1)
  197. }
  198. rawBytes = RenderIssueIndexPattern(rawBytes, urlPrefix, metas)
  199. rawBytes = RenderSha1CurrentPattern(rawBytes, urlPrefix)
  200. return rawBytes
  201. }
  202. func RenderSha1CurrentPattern(rawBytes []byte, urlPrefix string) []byte {
  203. ms := sha1CurrentPattern.FindAll(rawBytes, -1)
  204. for _, m := range ms {
  205. rawBytes = bytes.Replace(rawBytes, m, []byte(fmt.Sprintf(
  206. `<a href="%s/commit/%s"><code>%s</code></a>`, urlPrefix, m, ShortSha(string(m)))), -1)
  207. }
  208. return rawBytes
  209. }
  210. func RenderRawMarkdown(body []byte, urlPrefix string) []byte {
  211. htmlFlags := 0
  212. htmlFlags |= blackfriday.HTML_SKIP_STYLE
  213. htmlFlags |= blackfriday.HTML_OMIT_CONTENTS
  214. renderer := &CustomRender{
  215. Renderer: blackfriday.HtmlRenderer(htmlFlags, "", ""),
  216. urlPrefix: urlPrefix,
  217. }
  218. // set up the parser
  219. extensions := 0
  220. extensions |= blackfriday.EXTENSION_NO_INTRA_EMPHASIS
  221. extensions |= blackfriday.EXTENSION_TABLES
  222. extensions |= blackfriday.EXTENSION_FENCED_CODE
  223. extensions |= blackfriday.EXTENSION_AUTOLINK
  224. extensions |= blackfriday.EXTENSION_STRIKETHROUGH
  225. extensions |= blackfriday.EXTENSION_SPACE_HEADERS
  226. extensions |= blackfriday.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK
  227. if setting.Markdown.EnableHardLineBreak {
  228. extensions |= blackfriday.EXTENSION_HARD_LINE_BREAK
  229. }
  230. body = blackfriday.Markdown(body, renderer, extensions)
  231. return body
  232. }
  233. var (
  234. leftAngleBracket = []byte("</")
  235. rightAngleBracket = []byte(">")
  236. )
  237. var noEndTags = []string{"img", "input", "br", "hr"}
  238. // PostProcessMarkdown treats different types of HTML differently,
  239. // and only renders special links for plain text blocks.
  240. func PostProcessMarkdown(rawHtml []byte, urlPrefix string, metas map[string]string) []byte {
  241. startTags := make([]string, 0, 5)
  242. var buf bytes.Buffer
  243. tokenizer := html.NewTokenizer(bytes.NewReader(rawHtml))
  244. OUTER_LOOP:
  245. for html.ErrorToken != tokenizer.Next() {
  246. token := tokenizer.Token()
  247. switch token.Type {
  248. case html.TextToken:
  249. buf.Write(RenderSpecialLink([]byte(token.String()), urlPrefix, metas))
  250. case html.StartTagToken:
  251. buf.WriteString(token.String())
  252. tagName := token.Data
  253. // If this is an excluded tag, we skip processing all output until a close tag is encountered.
  254. if strings.EqualFold("a", tagName) || strings.EqualFold("code", tagName) || strings.EqualFold("pre", tagName) {
  255. stackNum := 1
  256. for html.ErrorToken != tokenizer.Next() {
  257. token = tokenizer.Token()
  258. // Copy the token to the output verbatim
  259. buf.WriteString(token.String())
  260. if token.Type == html.StartTagToken {
  261. stackNum++
  262. }
  263. // If this is the close tag to the outer-most, we are done
  264. if token.Type == html.EndTagToken && strings.EqualFold(tagName, token.Data) {
  265. stackNum--
  266. if stackNum == 0 {
  267. break
  268. }
  269. }
  270. }
  271. continue OUTER_LOOP
  272. }
  273. if !com.IsSliceContainsStr(noEndTags, token.Data) {
  274. startTags = append(startTags, token.Data)
  275. }
  276. case html.EndTagToken:
  277. if len(startTags) == 0 {
  278. buf.WriteString(token.String())
  279. break
  280. }
  281. buf.Write(leftAngleBracket)
  282. buf.WriteString(startTags[len(startTags)-1])
  283. buf.Write(rightAngleBracket)
  284. startTags = startTags[:len(startTags)-1]
  285. default:
  286. buf.WriteString(token.String())
  287. }
  288. }
  289. if io.EOF == tokenizer.Err() {
  290. return buf.Bytes()
  291. }
  292. // If we are not at the end of the input, then some other parsing error has occurred,
  293. // so return the input verbatim.
  294. return rawHtml
  295. }
  296. func RenderMarkdown(rawBytes []byte, urlPrefix string, metas map[string]string) []byte {
  297. result := RenderRawMarkdown(rawBytes, urlPrefix)
  298. result = PostProcessMarkdown(result, urlPrefix, metas)
  299. result = Sanitizer.SanitizeBytes(result)
  300. return result
  301. }
  302. func RenderMarkdownString(raw, urlPrefix string, metas map[string]string) string {
  303. return string(RenderMarkdown([]byte(raw), urlPrefix, metas))
  304. }