loading up the forgejo repo on tangled to test page performance
at forgejo 393 lines 10 kB view raw
1// Copyright 2017 The Gitea Authors. All rights reserved. 2// SPDX-License-Identifier: MIT 3 4package markup 5 6import ( 7 "bytes" 8 "context" 9 "errors" 10 "fmt" 11 "io" 12 "net/url" 13 "path/filepath" 14 "strings" 15 "sync" 16 17 "forgejo.org/modules/git" 18 "forgejo.org/modules/setting" 19 "forgejo.org/modules/util" 20 21 "github.com/yuin/goldmark/ast" 22) 23 24type RenderMetaMode string 25 26const ( 27 RenderMetaAsDetails RenderMetaMode = "details" // default 28 RenderMetaAsNone RenderMetaMode = "none" 29 RenderMetaAsTable RenderMetaMode = "table" 30) 31 32type ProcessorHelper struct { 33 IsUsernameMentionable func(ctx context.Context, username string) bool 34 GetRepoFileBlob func(ctx context.Context, ownerName, repoName, commitSha, filePath string, language *string) (*git.Blob, error) 35 36 ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute 37} 38 39var DefaultProcessorHelper ProcessorHelper 40 41// Init initialize regexps for markdown parsing 42func Init(ph *ProcessorHelper) { 43 if ph != nil { 44 DefaultProcessorHelper = *ph 45 } 46 47 NewSanitizer() 48 if len(setting.Markdown.CustomURLSchemes) > 0 { 49 CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes) 50 } 51 52 // since setting maybe changed extensions, this will reload all renderer extensions mapping 53 extRenderers = make(map[string]Renderer) 54 for _, renderer := range renderers { 55 for _, ext := range renderer.Extensions() { 56 extRenderers[strings.ToLower(ext)] = renderer 57 } 58 } 59} 60 61// Header holds the data about a header. 62type Header struct { 63 Level int 64 Text string 65 ID string 66} 67 68// RenderContext represents a render context 69type RenderContext struct { 70 Ctx context.Context 71 RelativePath string // relative path from tree root of the branch 72 Type string 73 IsWiki bool 74 Links Links 75 Metas map[string]string 76 DefaultLink string 77 GitRepo *git.Repository 78 ShaExistCache map[string]bool 79 cancelFn func() 80 SidebarTocNode ast.Node 81 InStandalonePage bool // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page 82} 83 84type Links struct { 85 AbsolutePrefix bool 86 Base string 87 BranchPath string 88 TreePath string 89} 90 91func (l *Links) Prefix() string { 92 if l.AbsolutePrefix { 93 return setting.AppURL 94 } 95 return setting.AppSubURL 96} 97 98func (l *Links) HasBranchInfo() bool { 99 return l.BranchPath != "" 100} 101 102func (l *Links) SrcLink() string { 103 return util.URLJoin(l.Base, "src", l.BranchPath, l.TreePath) 104} 105 106func (l *Links) MediaLink() string { 107 return util.URLJoin(l.Base, "media", l.BranchPath, l.TreePath) 108} 109 110func (l *Links) RawLink() string { 111 return util.URLJoin(l.Base, "raw", l.BranchPath, l.TreePath) 112} 113 114func (l *Links) WikiLink() string { 115 return util.URLJoin(l.Base, "wiki") 116} 117 118func (l *Links) WikiRawLink() string { 119 return util.URLJoin(l.Base, "wiki/raw") 120} 121 122func (l *Links) ResolveMediaLink(isWiki bool) string { 123 if isWiki { 124 return l.WikiRawLink() 125 } else if l.HasBranchInfo() { 126 return l.MediaLink() 127 } 128 return l.Base 129} 130 131// Cancel runs any cleanup functions that have been registered for this Ctx 132func (ctx *RenderContext) Cancel() { 133 if ctx == nil { 134 return 135 } 136 ctx.ShaExistCache = map[string]bool{} 137 if ctx.cancelFn == nil { 138 return 139 } 140 ctx.cancelFn() 141} 142 143// AddCancel adds the provided fn as a Cleanup for this Ctx 144func (ctx *RenderContext) AddCancel(fn func()) { 145 if ctx == nil { 146 return 147 } 148 oldCancelFn := ctx.cancelFn 149 if oldCancelFn == nil { 150 ctx.cancelFn = fn 151 return 152 } 153 ctx.cancelFn = func() { 154 defer oldCancelFn() 155 fn() 156 } 157} 158 159// Renderer defines an interface for rendering markup file to HTML 160type Renderer interface { 161 Name() string // markup format name 162 Extensions() []string 163 SanitizerRules() []setting.MarkupSanitizerRule 164 Render(ctx *RenderContext, input io.Reader, output io.Writer) error 165} 166 167// PostProcessRenderer defines an interface for renderers who need post process 168type PostProcessRenderer interface { 169 NeedPostProcess() bool 170} 171 172// PostProcessRenderer defines an interface for external renderers 173type ExternalRenderer interface { 174 // SanitizerDisabled disabled sanitize if return true 175 SanitizerDisabled() bool 176 177 // DisplayInIFrame represents whether render the content with an iframe 178 DisplayInIFrame() bool 179} 180 181// RendererContentDetector detects if the content can be rendered 182// by specified renderer 183type RendererContentDetector interface { 184 CanRender(filename string, input io.Reader) bool 185} 186 187var ( 188 extRenderers = make(map[string]Renderer) 189 renderers = make(map[string]Renderer) 190) 191 192// RegisterRenderer registers a new markup file renderer 193func RegisterRenderer(renderer Renderer) { 194 renderers[renderer.Name()] = renderer 195 for _, ext := range renderer.Extensions() { 196 extRenderers[strings.ToLower(ext)] = renderer 197 } 198} 199 200// GetRendererByFileName get renderer by filename 201func GetRendererByFileName(filename string) Renderer { 202 extension := strings.ToLower(filepath.Ext(filename)) 203 return extRenderers[extension] 204} 205 206// GetRendererByType returns a renderer according type 207func GetRendererByType(tp string) Renderer { 208 return renderers[tp] 209} 210 211// DetectRendererType detects the markup type of the content 212func DetectRendererType(filename string, input io.Reader) string { 213 buf, err := io.ReadAll(input) 214 if err != nil { 215 return "" 216 } 217 for _, renderer := range renderers { 218 if detector, ok := renderer.(RendererContentDetector); ok && detector.CanRender(filename, bytes.NewReader(buf)) { 219 return renderer.Name() 220 } 221 } 222 return "" 223} 224 225// Render renders markup file to HTML with all specific handling stuff. 226func Render(ctx *RenderContext, input io.Reader, output io.Writer) error { 227 if ctx.Type != "" { 228 return renderByType(ctx, input, output) 229 } else if ctx.RelativePath != "" { 230 return renderFile(ctx, input, output) 231 } 232 return errors.New("Render options both filename and type missing") 233} 234 235// RenderString renders Markup string to HTML with all specific handling stuff and return string 236func RenderString(ctx *RenderContext, content string) (string, error) { 237 var buf strings.Builder 238 if err := Render(ctx, strings.NewReader(content), &buf); err != nil { 239 return "", err 240 } 241 return buf.String(), nil 242} 243 244type nopCloser struct { 245 io.Writer 246} 247 248func (nopCloser) Close() error { return nil } 249 250func renderIFrame(ctx *RenderContext, output io.Writer) error { 251 // set height="0" ahead, otherwise the scrollHeight would be max(150, realHeight) 252 // at the moment, only "allow-scripts" is allowed for sandbox mode. 253 // "allow-same-origin" should never be used, it leads to XSS attack, and it makes the JS in iframe can access parent window's config and CSRF token 254 // TODO: when using dark theme, if the rendered content doesn't have proper style, the default text color is black, which is not easy to read 255 _, err := io.WriteString(output, fmt.Sprintf(` 256<iframe src="%s/%s/%s/render/%s/%s" 257name="giteaExternalRender" 258onload="this.height=giteaExternalRender.document.documentElement.scrollHeight" 259width="100%%" height="0" scrolling="no" frameborder="0" style="overflow: hidden" 260sandbox="allow-scripts" 261></iframe>`, 262 setting.AppSubURL, 263 url.PathEscape(ctx.Metas["user"]), 264 url.PathEscape(ctx.Metas["repo"]), 265 ctx.Metas["BranchNameSubURL"], 266 url.PathEscape(ctx.RelativePath), 267 )) 268 return err 269} 270 271func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error { 272 var wg sync.WaitGroup 273 var err error 274 pr, pw := io.Pipe() 275 defer func() { 276 _ = pr.Close() 277 _ = pw.Close() 278 }() 279 280 var pr2 io.ReadCloser 281 var pw2 io.WriteCloser 282 283 var sanitizerDisabled bool 284 if r, ok := renderer.(ExternalRenderer); ok { 285 sanitizerDisabled = r.SanitizerDisabled() 286 } 287 288 if !sanitizerDisabled { 289 pr2, pw2 = io.Pipe() 290 defer func() { 291 _ = pr2.Close() 292 _ = pw2.Close() 293 }() 294 295 wg.Add(1) 296 go func() { 297 err = SanitizeReader(pr2, renderer.Name(), output) 298 _ = pr2.Close() 299 wg.Done() 300 }() 301 } else { 302 pw2 = nopCloser{output} 303 } 304 305 wg.Add(1) 306 go func() { 307 if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() { 308 err = PostProcess(ctx, pr, pw2) 309 } else { 310 _, err = io.Copy(pw2, pr) 311 } 312 _ = pr.Close() 313 _ = pw2.Close() 314 wg.Done() 315 }() 316 317 if err1 := renderer.Render(ctx, input, pw); err1 != nil { 318 return err1 319 } 320 _ = pw.Close() 321 322 wg.Wait() 323 return err 324} 325 326// ErrUnsupportedRenderType represents 327type ErrUnsupportedRenderType struct { 328 Type string 329} 330 331func (err ErrUnsupportedRenderType) Error() string { 332 return fmt.Sprintf("Unsupported render type: %s", err.Type) 333} 334 335func renderByType(ctx *RenderContext, input io.Reader, output io.Writer) error { 336 if renderer, ok := renderers[ctx.Type]; ok { 337 return render(ctx, renderer, input, output) 338 } 339 return ErrUnsupportedRenderType{ctx.Type} 340} 341 342// ErrUnsupportedRenderExtension represents the error when extension doesn't supported to render 343type ErrUnsupportedRenderExtension struct { 344 Extension string 345} 346 347func IsErrUnsupportedRenderExtension(err error) bool { 348 _, ok := err.(ErrUnsupportedRenderExtension) 349 return ok 350} 351 352func (err ErrUnsupportedRenderExtension) Error() string { 353 return fmt.Sprintf("Unsupported render extension: %s", err.Extension) 354} 355 356func renderFile(ctx *RenderContext, input io.Reader, output io.Writer) error { 357 extension := strings.ToLower(filepath.Ext(ctx.RelativePath)) 358 if renderer, ok := extRenderers[extension]; ok { 359 if r, ok := renderer.(ExternalRenderer); ok && r.DisplayInIFrame() { 360 if !ctx.InStandalonePage { 361 // for an external render, it could only output its content in a standalone page 362 // otherwise, a <iframe> should be outputted to embed the external rendered page 363 return renderIFrame(ctx, output) 364 } 365 } 366 return render(ctx, renderer, input, output) 367 } 368 return ErrUnsupportedRenderExtension{extension} 369} 370 371// Type returns if markup format via the filename 372func Type(filename string) string { 373 if parser := GetRendererByFileName(filename); parser != nil { 374 return parser.Name() 375 } 376 return "" 377} 378 379// IsMarkupFile reports whether file is a markup type file 380func IsMarkupFile(name, markup string) bool { 381 if parser := GetRendererByFileName(name); parser != nil { 382 return parser.Name() == markup 383 } 384 return false 385} 386 387func PreviewableExtensions() []string { 388 extensions := make([]string, 0, len(extRenderers)) 389 for extension := range extRenderers { 390 extensions = append(extensions, extension) 391 } 392 return extensions 393}