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