gitea源码

markdown.go 8.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2018 The Gitea Authors. All rights reserved.
  3. // SPDX-License-Identifier: MIT
  4. package markdown
  5. import (
  6. "errors"
  7. "html/template"
  8. "io"
  9. "strings"
  10. "code.gitea.io/gitea/modules/log"
  11. "code.gitea.io/gitea/modules/markup"
  12. "code.gitea.io/gitea/modules/markup/common"
  13. "code.gitea.io/gitea/modules/markup/markdown/math"
  14. "code.gitea.io/gitea/modules/setting"
  15. giteautil "code.gitea.io/gitea/modules/util"
  16. chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
  17. "github.com/yuin/goldmark"
  18. highlighting "github.com/yuin/goldmark-highlighting/v2"
  19. meta "github.com/yuin/goldmark-meta"
  20. "github.com/yuin/goldmark/extension"
  21. "github.com/yuin/goldmark/parser"
  22. "github.com/yuin/goldmark/renderer"
  23. "github.com/yuin/goldmark/renderer/html"
  24. "github.com/yuin/goldmark/util"
  25. )
  26. var (
  27. renderContextKey = parser.NewContextKey()
  28. renderConfigKey = parser.NewContextKey()
  29. )
  30. type limitWriter struct {
  31. w io.Writer
  32. sum int64
  33. limit int64
  34. }
  35. // Write implements the standard Write interface:
  36. func (l *limitWriter) Write(data []byte) (int, error) {
  37. leftToWrite := l.limit - l.sum
  38. if leftToWrite < int64(len(data)) {
  39. n, err := l.w.Write(data[:leftToWrite])
  40. l.sum += int64(n)
  41. if err != nil {
  42. return n, err
  43. }
  44. return n, errors.New("rendered content too large - truncating render")
  45. }
  46. n, err := l.w.Write(data)
  47. l.sum += int64(n)
  48. return n, err
  49. }
  50. // newParserContext creates a parser.Context with the render context set
  51. func newParserContext(ctx *markup.RenderContext) parser.Context {
  52. pc := parser.NewContext(parser.WithIDs(newPrefixedIDs()))
  53. pc.Set(renderContextKey, ctx)
  54. return pc
  55. }
  56. type GlodmarkRender struct {
  57. ctx *markup.RenderContext
  58. goldmarkMarkdown goldmark.Markdown
  59. }
  60. func (r *GlodmarkRender) Convert(source []byte, writer io.Writer, opts ...parser.ParseOption) error {
  61. return r.goldmarkMarkdown.Convert(source, writer, opts...)
  62. }
  63. func (r *GlodmarkRender) Renderer() renderer.Renderer {
  64. return r.goldmarkMarkdown.Renderer()
  65. }
  66. func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
  67. if entering {
  68. languageBytes, _ := c.Language()
  69. languageStr := giteautil.IfZero(string(languageBytes), "text")
  70. preClasses := "code-block"
  71. if languageStr == "mermaid" || languageStr == "math" {
  72. preClasses += " is-loading"
  73. }
  74. // include language-x class as part of commonmark spec, "chroma" class is used to highlight the code
  75. // the "display" class is used by "js/markup/math.ts" to render the code element as a block
  76. // the "math.ts" strictly depends on the structure: <pre class="code-block is-loading"><code class="language-math display">...</code></pre>
  77. err := r.ctx.RenderInternal.FormatWithSafeAttrs(w, `<div class="code-block-container code-overflow-scroll"><pre class="%s"><code class="chroma language-%s display">`, preClasses, languageStr)
  78. if err != nil {
  79. return
  80. }
  81. } else {
  82. _, err := w.WriteString("</code></pre></div>")
  83. if err != nil {
  84. return
  85. }
  86. }
  87. }
  88. // SpecializedMarkdown sets up the Gitea specific markdown extensions
  89. func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender {
  90. // TODO: it could use a pool to cache the renderers to reuse them with different contexts
  91. // at the moment it is fast enough (see the benchmarks)
  92. r := &GlodmarkRender{ctx: ctx}
  93. r.goldmarkMarkdown = goldmark.New(
  94. goldmark.WithExtensions(
  95. extension.NewTable(extension.WithTableCellAlignMethod(extension.TableCellAlignAttribute)),
  96. extension.Strikethrough,
  97. extension.TaskList,
  98. extension.DefinitionList,
  99. common.FootnoteExtension,
  100. highlighting.NewHighlighting(
  101. highlighting.WithFormatOptions(
  102. chromahtml.WithClasses(true),
  103. chromahtml.PreventSurroundingPre(true),
  104. ),
  105. highlighting.WithWrapperRenderer(r.highlightingRenderer),
  106. ),
  107. math.NewExtension(&ctx.RenderInternal, math.Options{
  108. Enabled: setting.Markdown.EnableMath,
  109. ParseInlineDollar: setting.Markdown.MathCodeBlockOptions.ParseInlineDollar,
  110. ParseInlineParentheses: setting.Markdown.MathCodeBlockOptions.ParseInlineParentheses, // this is a bad syntax "\( ... \)", it conflicts with normal markdown escaping
  111. ParseBlockDollar: setting.Markdown.MathCodeBlockOptions.ParseBlockDollar,
  112. ParseBlockSquareBrackets: setting.Markdown.MathCodeBlockOptions.ParseBlockSquareBrackets, // this is a bad syntax "\[ ... \]", it conflicts with normal markdown escaping
  113. }),
  114. meta.Meta,
  115. ),
  116. goldmark.WithParserOptions(
  117. parser.WithAttribute(),
  118. parser.WithAutoHeadingID(),
  119. parser.WithASTTransformers(util.Prioritized(NewASTTransformer(&ctx.RenderInternal), 10000)),
  120. ),
  121. goldmark.WithRendererOptions(html.WithUnsafe()),
  122. )
  123. // Override the original Tasklist renderer!
  124. r.goldmarkMarkdown.Renderer().AddOptions(
  125. renderer.WithNodeRenderers(util.Prioritized(NewHTMLRenderer(&ctx.RenderInternal), 10)),
  126. )
  127. return r
  128. }
  129. // render calls goldmark render to convert Markdown to HTML
  130. // NOTE: The output of this method MUST get sanitized separately!!!
  131. func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
  132. converter := SpecializedMarkdown(ctx)
  133. lw := &limitWriter{
  134. w: output,
  135. limit: setting.UI.MaxDisplayFileSize * 3,
  136. }
  137. // FIXME: Don't read all to memory, but goldmark doesn't support
  138. buf, err := io.ReadAll(input)
  139. if err != nil {
  140. log.Error("Unable to ReadAll: %v", err)
  141. return err
  142. }
  143. buf = giteautil.NormalizeEOL(buf)
  144. // FIXME: should we include a timeout to abort the renderer if it takes too long?
  145. defer func() {
  146. err := recover()
  147. if err == nil {
  148. return
  149. }
  150. log.Error("Panic in markdown: %v\n%s", err, log.Stack(2))
  151. escapedHTML := template.HTMLEscapeString(giteautil.UnsafeBytesToString(buf))
  152. _, _ = output.Write(giteautil.UnsafeStringToBytes(escapedHTML))
  153. }()
  154. pc := newParserContext(ctx)
  155. // Preserve original length.
  156. bufWithMetadataLength := len(buf)
  157. rc := &RenderConfig{Meta: markup.RenderMetaAsDetails}
  158. buf, _ = ExtractMetadataBytes(buf, rc)
  159. metaLength := max(bufWithMetadataLength-len(buf), 0)
  160. rc.metaLength = metaLength
  161. pc.Set(renderConfigKey, rc)
  162. if err := converter.Convert(buf, lw, parser.WithContext(pc)); err != nil {
  163. log.Error("Unable to render: %v", err)
  164. return err
  165. }
  166. return nil
  167. }
  168. // MarkupName describes markup's name
  169. var MarkupName = "markdown"
  170. func init() {
  171. markup.RegisterRenderer(Renderer{})
  172. }
  173. // Renderer implements markup.Renderer
  174. type Renderer struct{}
  175. var _ markup.PostProcessRenderer = (*Renderer)(nil)
  176. // Name implements markup.Renderer
  177. func (Renderer) Name() string {
  178. return MarkupName
  179. }
  180. // NeedPostProcess implements markup.PostProcessRenderer
  181. func (Renderer) NeedPostProcess() bool { return true }
  182. // Extensions implements markup.Renderer
  183. func (Renderer) Extensions() []string {
  184. return setting.Markdown.FileExtensions
  185. }
  186. // SanitizerRules implements markup.Renderer
  187. func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
  188. return []setting.MarkupSanitizerRule{}
  189. }
  190. // Render implements markup.Renderer
  191. func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
  192. return render(ctx, input, output)
  193. }
  194. // Render renders Markdown to HTML with all specific handling stuff.
  195. func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
  196. ctx.RenderOptions.MarkupType = MarkupName
  197. return markup.Render(ctx, input, output)
  198. }
  199. // RenderString renders Markdown string to HTML with all specific handling stuff and return string
  200. func RenderString(ctx *markup.RenderContext, content string) (template.HTML, error) {
  201. var buf strings.Builder
  202. if err := Render(ctx, strings.NewReader(content), &buf); err != nil {
  203. return "", err
  204. }
  205. return template.HTML(buf.String()), nil
  206. }
  207. // RenderRaw renders Markdown to HTML without handling special links.
  208. func RenderRaw(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
  209. rd, wr := io.Pipe()
  210. defer func() {
  211. _ = rd.Close()
  212. _ = wr.Close()
  213. }()
  214. go func() {
  215. if err := render(ctx, input, wr); err != nil {
  216. _ = wr.CloseWithError(err)
  217. return
  218. }
  219. _ = wr.Close()
  220. }()
  221. return markup.SanitizeReader(rd, "", output)
  222. }
  223. // RenderRawString renders Markdown to HTML without handling special links and return string
  224. func RenderRawString(ctx *markup.RenderContext, content string) (string, error) {
  225. var buf strings.Builder
  226. if err := RenderRaw(ctx, strings.NewReader(content), &buf); err != nil {
  227. return "", err
  228. }
  229. return buf.String(), nil
  230. }