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.

markdown.go 6.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2018 The Gitea Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package markdown
  6. import (
  7. "bytes"
  8. "io"
  9. "strings"
  10. "code.gitea.io/gitea/modules/markup"
  11. "code.gitea.io/gitea/modules/setting"
  12. "code.gitea.io/gitea/modules/util"
  13. "github.com/russross/blackfriday/v2"
  14. )
  15. // Renderer is a extended version of underlying render object.
  16. type Renderer struct {
  17. blackfriday.Renderer
  18. URLPrefix string
  19. IsWiki bool
  20. }
  21. var byteMailto = []byte("mailto:")
  22. var htmlEscaper = [256][]byte{
  23. '&': []byte("&"),
  24. '<': []byte("&lt;"),
  25. '>': []byte("&gt;"),
  26. '"': []byte("&quot;"),
  27. }
  28. func escapeHTML(w io.Writer, s []byte) {
  29. var start, end int
  30. for end < len(s) {
  31. escSeq := htmlEscaper[s[end]]
  32. if escSeq != nil {
  33. _, _ = w.Write(s[start:end])
  34. _, _ = w.Write(escSeq)
  35. start = end + 1
  36. }
  37. end++
  38. }
  39. if start < len(s) && end <= len(s) {
  40. _, _ = w.Write(s[start:end])
  41. }
  42. }
  43. // RenderNode is a default renderer of a single node of a syntax tree. For
  44. // block nodes it will be called twice: first time with entering=true, second
  45. // time with entering=false, so that it could know when it's working on an open
  46. // tag and when on close. It writes the result to w.
  47. //
  48. // The return value is a way to tell the calling walker to adjust its walk
  49. // pattern: e.g. it can terminate the traversal by returning Terminate. Or it
  50. // can ask the walker to skip a subtree of this node by returning SkipChildren.
  51. // The typical behavior is to return GoToNext, which asks for the usual
  52. // traversal to the next node.
  53. func (r *Renderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
  54. switch node.Type {
  55. case blackfriday.Image:
  56. prefix := r.URLPrefix
  57. if r.IsWiki {
  58. prefix = util.URLJoin(prefix, "wiki", "raw")
  59. }
  60. prefix = strings.Replace(prefix, "/src/", "/media/", 1)
  61. link := node.LinkData.Destination
  62. if len(link) > 0 && !markup.IsLink(link) {
  63. lnk := string(link)
  64. lnk = util.URLJoin(prefix, lnk)
  65. lnk = strings.Replace(lnk, " ", "+", -1)
  66. link = []byte(lnk)
  67. }
  68. node.LinkData.Destination = link
  69. // Render link around image only if parent is not link already
  70. if node.Parent != nil && node.Parent.Type != blackfriday.Link {
  71. if entering {
  72. _, _ = w.Write([]byte(`<a href="`))
  73. escapeHTML(w, link)
  74. _, _ = w.Write([]byte(`">`))
  75. return r.Renderer.RenderNode(w, node, entering)
  76. }
  77. s := r.Renderer.RenderNode(w, node, entering)
  78. _, _ = w.Write([]byte(`</a>`))
  79. return s
  80. }
  81. return r.Renderer.RenderNode(w, node, entering)
  82. case blackfriday.Link:
  83. // special case: this is not a link, a hash link or a mailto:, so it's a
  84. // relative URL
  85. link := node.LinkData.Destination
  86. if len(link) > 0 && !markup.IsLink(link) &&
  87. link[0] != '#' && !bytes.HasPrefix(link, byteMailto) &&
  88. node.LinkData.Footnote == nil {
  89. lnk := string(link)
  90. if r.IsWiki {
  91. lnk = util.URLJoin("wiki", lnk)
  92. }
  93. link = []byte(util.URLJoin(r.URLPrefix, lnk))
  94. }
  95. node.LinkData.Destination = link
  96. return r.Renderer.RenderNode(w, node, entering)
  97. case blackfriday.Text:
  98. isListItem := false
  99. for n := node.Parent; n != nil; n = n.Parent {
  100. if n.Type == blackfriday.Item {
  101. isListItem = true
  102. break
  103. }
  104. }
  105. if isListItem {
  106. text := node.Literal
  107. switch {
  108. case bytes.HasPrefix(text, []byte("[ ] ")):
  109. _, _ = w.Write([]byte(`<span class="ui fitted disabled checkbox"><input type="checkbox" disabled="disabled" /><label /></span>`))
  110. text = text[3:]
  111. case bytes.HasPrefix(text, []byte("[x] ")):
  112. _, _ = w.Write([]byte(`<span class="ui checked fitted disabled checkbox"><input type="checkbox" checked="" disabled="disabled" /><label /></span>`))
  113. text = text[3:]
  114. }
  115. node.Literal = text
  116. }
  117. }
  118. return r.Renderer.RenderNode(w, node, entering)
  119. }
  120. const (
  121. blackfridayExtensions = 0 |
  122. blackfriday.NoIntraEmphasis |
  123. blackfriday.Tables |
  124. blackfriday.FencedCode |
  125. blackfriday.Strikethrough |
  126. blackfriday.NoEmptyLineBeforeBlock |
  127. blackfriday.DefinitionLists |
  128. blackfriday.Footnotes |
  129. blackfriday.HeadingIDs |
  130. blackfriday.AutoHeadingIDs
  131. blackfridayHTMLFlags = 0 |
  132. blackfriday.Smartypants
  133. )
  134. // RenderRaw renders Markdown to HTML without handling special links.
  135. func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte {
  136. renderer := &Renderer{
  137. Renderer: blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{
  138. Flags: blackfridayHTMLFlags,
  139. }),
  140. URLPrefix: urlPrefix,
  141. IsWiki: wikiMarkdown,
  142. }
  143. exts := blackfridayExtensions
  144. if setting.Markdown.EnableHardLineBreak {
  145. exts |= blackfriday.HardLineBreak
  146. }
  147. // Need to normalize EOL to UNIX LF to have consistent results in rendering
  148. body = blackfriday.Run(util.NormalizeEOL(body), blackfriday.WithRenderer(renderer), blackfriday.WithExtensions(exts))
  149. return markup.SanitizeBytes(body)
  150. }
  151. var (
  152. // MarkupName describes markup's name
  153. MarkupName = "markdown"
  154. )
  155. func init() {
  156. markup.RegisterParser(Parser{})
  157. }
  158. // Parser implements markup.Parser
  159. type Parser struct {
  160. }
  161. // Name implements markup.Parser
  162. func (Parser) Name() string {
  163. return MarkupName
  164. }
  165. // Extensions implements markup.Parser
  166. func (Parser) Extensions() []string {
  167. return setting.Markdown.FileExtensions
  168. }
  169. // Render implements markup.Parser
  170. func (Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte {
  171. return RenderRaw(rawBytes, urlPrefix, isWiki)
  172. }
  173. // Render renders Markdown to HTML with all specific handling stuff.
  174. func Render(rawBytes []byte, urlPrefix string, metas map[string]string) []byte {
  175. return markup.Render("a.md", rawBytes, urlPrefix, metas)
  176. }
  177. // RenderString renders Markdown to HTML with special links and returns string type.
  178. func RenderString(raw, urlPrefix string, metas map[string]string) string {
  179. return markup.RenderString("a.md", raw, urlPrefix, metas)
  180. }
  181. // RenderWiki renders markdown wiki page to HTML and return HTML string
  182. func RenderWiki(rawBytes []byte, urlPrefix string, metas map[string]string) string {
  183. return markup.RenderWiki("a.md", rawBytes, urlPrefix, metas)
  184. }
  185. // IsMarkdownFile reports whether name looks like a Markdown file
  186. // based on its extension.
  187. func IsMarkdownFile(name string) bool {
  188. return markup.IsMarkupFile(name, MarkupName)
  189. }