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