loading up the forgejo repo on tangled to test page performance
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}