code.gitea.io/gitea@v1.19.3/modules/markup/markdown/markdown.go (about) 1 // Copyright 2014 The Gogs Authors. All rights reserved. 2 // Copyright 2018 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package markdown 6 7 import ( 8 "fmt" 9 "io" 10 "strings" 11 "sync" 12 13 "code.gitea.io/gitea/modules/log" 14 "code.gitea.io/gitea/modules/markup" 15 "code.gitea.io/gitea/modules/markup/common" 16 "code.gitea.io/gitea/modules/markup/markdown/math" 17 "code.gitea.io/gitea/modules/setting" 18 giteautil "code.gitea.io/gitea/modules/util" 19 20 chromahtml "github.com/alecthomas/chroma/v2/formatters/html" 21 "github.com/yuin/goldmark" 22 highlighting "github.com/yuin/goldmark-highlighting/v2" 23 meta "github.com/yuin/goldmark-meta" 24 "github.com/yuin/goldmark/extension" 25 "github.com/yuin/goldmark/parser" 26 "github.com/yuin/goldmark/renderer" 27 "github.com/yuin/goldmark/renderer/html" 28 "github.com/yuin/goldmark/util" 29 ) 30 31 var ( 32 converter goldmark.Markdown 33 once = sync.Once{} 34 ) 35 36 var ( 37 urlPrefixKey = parser.NewContextKey() 38 isWikiKey = parser.NewContextKey() 39 renderMetasKey = parser.NewContextKey() 40 renderContextKey = parser.NewContextKey() 41 renderConfigKey = parser.NewContextKey() 42 ) 43 44 type limitWriter struct { 45 w io.Writer 46 sum int64 47 limit int64 48 } 49 50 // Write implements the standard Write interface: 51 func (l *limitWriter) Write(data []byte) (int, error) { 52 leftToWrite := l.limit - l.sum 53 if leftToWrite < int64(len(data)) { 54 n, err := l.w.Write(data[:leftToWrite]) 55 l.sum += int64(n) 56 if err != nil { 57 return n, err 58 } 59 return n, fmt.Errorf("Rendered content too large - truncating render") 60 } 61 n, err := l.w.Write(data) 62 l.sum += int64(n) 63 return n, err 64 } 65 66 // newParserContext creates a parser.Context with the render context set 67 func newParserContext(ctx *markup.RenderContext) parser.Context { 68 pc := parser.NewContext(parser.WithIDs(newPrefixedIDs())) 69 pc.Set(urlPrefixKey, ctx.URLPrefix) 70 pc.Set(isWikiKey, ctx.IsWiki) 71 pc.Set(renderMetasKey, ctx.Metas) 72 pc.Set(renderContextKey, ctx) 73 return pc 74 } 75 76 // actualRender renders Markdown to HTML without handling special links. 77 func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { 78 once.Do(func() { 79 converter = goldmark.New( 80 goldmark.WithExtensions( 81 extension.NewTable( 82 extension.WithTableCellAlignMethod(extension.TableCellAlignAttribute)), 83 extension.Strikethrough, 84 extension.TaskList, 85 extension.DefinitionList, 86 common.FootnoteExtension, 87 highlighting.NewHighlighting( 88 highlighting.WithFormatOptions( 89 chromahtml.WithClasses(true), 90 chromahtml.PreventSurroundingPre(true), 91 ), 92 highlighting.WithWrapperRenderer(func(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) { 93 if entering { 94 language, _ := c.Language() 95 if language == nil { 96 language = []byte("text") 97 } 98 99 languageStr := string(language) 100 101 preClasses := []string{"code-block"} 102 if languageStr == "mermaid" || languageStr == "math" { 103 preClasses = append(preClasses, "is-loading") 104 } 105 106 _, err := w.WriteString(`<pre class="` + strings.Join(preClasses, " ") + `">`) 107 if err != nil { 108 return 109 } 110 111 // include language-x class as part of commonmark spec 112 _, err = w.WriteString(`<code class="chroma language-` + string(language) + `">`) 113 if err != nil { 114 return 115 } 116 } else { 117 _, err := w.WriteString("</code></pre>") 118 if err != nil { 119 return 120 } 121 } 122 }), 123 ), 124 math.NewExtension( 125 math.Enabled(setting.Markdown.EnableMath), 126 ), 127 meta.Meta, 128 ), 129 goldmark.WithParserOptions( 130 parser.WithAttribute(), 131 parser.WithAutoHeadingID(), 132 parser.WithASTTransformers( 133 util.Prioritized(&ASTTransformer{}, 10000), 134 ), 135 ), 136 goldmark.WithRendererOptions( 137 html.WithUnsafe(), 138 ), 139 ) 140 141 // Override the original Tasklist renderer! 142 converter.Renderer().AddOptions( 143 renderer.WithNodeRenderers( 144 util.Prioritized(NewHTMLRenderer(), 10), 145 ), 146 ) 147 }) 148 149 lw := &limitWriter{ 150 w: output, 151 limit: setting.UI.MaxDisplayFileSize * 3, 152 } 153 154 // FIXME: should we include a timeout to abort the renderer if it takes too long? 155 defer func() { 156 err := recover() 157 if err == nil { 158 return 159 } 160 161 log.Warn("Unable to render markdown due to panic in goldmark: %v", err) 162 if log.IsDebug() { 163 log.Debug("Panic in markdown: %v\n%s", err, log.Stack(2)) 164 } 165 }() 166 167 // FIXME: Don't read all to memory, but goldmark doesn't support 168 pc := newParserContext(ctx) 169 buf, err := io.ReadAll(input) 170 if err != nil { 171 log.Error("Unable to ReadAll: %v", err) 172 return err 173 } 174 buf = giteautil.NormalizeEOL(buf) 175 176 rc := &RenderConfig{ 177 Meta: "table", 178 Icon: "table", 179 Lang: "", 180 } 181 buf, _ = ExtractMetadataBytes(buf, rc) 182 183 pc.Set(renderConfigKey, rc) 184 185 if err := converter.Convert(buf, lw, parser.WithContext(pc)); err != nil { 186 log.Error("Unable to render: %v", err) 187 return err 188 } 189 190 return nil 191 } 192 193 // Note: The output of this method must get sanitized. 194 func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { 195 defer func() { 196 err := recover() 197 if err == nil { 198 return 199 } 200 201 log.Warn("Unable to render markdown due to panic in goldmark - will return raw bytes") 202 if log.IsDebug() { 203 log.Debug("Panic in markdown: %v\n%s", err, log.Stack(2)) 204 } 205 _, err = io.Copy(output, input) 206 if err != nil { 207 log.Error("io.Copy failed: %v", err) 208 } 209 }() 210 return actualRender(ctx, input, output) 211 } 212 213 // MarkupName describes markup's name 214 var MarkupName = "markdown" 215 216 func init() { 217 markup.RegisterRenderer(Renderer{}) 218 } 219 220 // Renderer implements markup.Renderer 221 type Renderer struct{} 222 223 var _ markup.PostProcessRenderer = (*Renderer)(nil) 224 225 // Name implements markup.Renderer 226 func (Renderer) Name() string { 227 return MarkupName 228 } 229 230 // NeedPostProcess implements markup.PostProcessRenderer 231 func (Renderer) NeedPostProcess() bool { return true } 232 233 // Extensions implements markup.Renderer 234 func (Renderer) Extensions() []string { 235 return setting.Markdown.FileExtensions 236 } 237 238 // SanitizerRules implements markup.Renderer 239 func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { 240 return []setting.MarkupSanitizerRule{} 241 } 242 243 // Render implements markup.Renderer 244 func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { 245 return render(ctx, input, output) 246 } 247 248 // Render renders Markdown to HTML with all specific handling stuff. 249 func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { 250 if ctx.Type == "" { 251 ctx.Type = MarkupName 252 } 253 return markup.Render(ctx, input, output) 254 } 255 256 // RenderString renders Markdown string to HTML with all specific handling stuff and return string 257 func RenderString(ctx *markup.RenderContext, content string) (string, error) { 258 var buf strings.Builder 259 if err := Render(ctx, strings.NewReader(content), &buf); err != nil { 260 return "", err 261 } 262 return buf.String(), nil 263 } 264 265 // RenderRaw renders Markdown to HTML without handling special links. 266 func RenderRaw(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { 267 rd, wr := io.Pipe() 268 defer func() { 269 _ = rd.Close() 270 _ = wr.Close() 271 }() 272 273 go func() { 274 if err := render(ctx, input, wr); err != nil { 275 _ = wr.CloseWithError(err) 276 return 277 } 278 _ = wr.Close() 279 }() 280 281 return markup.SanitizeReader(rd, "", output) 282 } 283 284 // RenderRawString renders Markdown to HTML without handling special links and return string 285 func RenderRawString(ctx *markup.RenderContext, content string) (string, error) { 286 var buf strings.Builder 287 if err := RenderRaw(ctx, strings.NewReader(content), &buf); err != nil { 288 return "", err 289 } 290 return buf.String(), nil 291 }