// Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package markup import ( "context" "fmt" "html/template" "io" "net/url" "strconv" "strings" "time" "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/markup/internal" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "github.com/yuin/goldmark/ast" "golang.org/x/sync/errgroup" ) type RenderMetaMode string const ( RenderMetaAsDetails RenderMetaMode = "details" // default RenderMetaAsNone RenderMetaMode = "none" RenderMetaAsTable RenderMetaMode = "table" ) var RenderBehaviorForTesting struct { // Gitea will emit some additional attributes for various purposes, these attributes don't affect rendering. // But there are too many hard-coded test cases, to avoid changing all of them again and again, we can disable emitting these internal attributes. DisableAdditionalAttributes bool } type RenderOptions struct { UseAbsoluteLink bool // relative path from tree root of the branch RelativePath string // eg: "orgmode", "asciicast", "console" // for file mode, it could be left as empty, and will be detected by file extension in RelativePath MarkupType string // user&repo, format&style®exp (for external issue pattern), teams&org (for mention) // RefTypeNameSubURL (for iframe&asciicast) // markupAllowShortIssuePattern // markdownNewLineHardBreak Metas map[string]string // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page InStandalonePage bool } // RenderContext represents a render context type RenderContext struct { ctx context.Context // the context might be used by the "render" function, but it might also be used by "postProcess" function usedByRender bool SidebarTocNode ast.Node RenderHelper RenderHelper RenderOptions RenderOptions RenderInternal internal.RenderInternal } func (ctx *RenderContext) Deadline() (deadline time.Time, ok bool) { return ctx.ctx.Deadline() } func (ctx *RenderContext) Done() <-chan struct{} { return ctx.ctx.Done() } func (ctx *RenderContext) Err() error { return ctx.ctx.Err() } func (ctx *RenderContext) Value(key any) any { return ctx.ctx.Value(key) } var _ context.Context = (*RenderContext)(nil) func NewRenderContext(ctx context.Context) *RenderContext { return &RenderContext{ctx: ctx, RenderHelper: &SimpleRenderHelper{}} } func (ctx *RenderContext) WithMarkupType(typ string) *RenderContext { ctx.RenderOptions.MarkupType = typ return ctx } func (ctx *RenderContext) WithRelativePath(path string) *RenderContext { ctx.RenderOptions.RelativePath = path return ctx } func (ctx *RenderContext) WithMetas(metas map[string]string) *RenderContext { ctx.RenderOptions.Metas = metas return ctx } func (ctx *RenderContext) WithInStandalonePage(v bool) *RenderContext { ctx.RenderOptions.InStandalonePage = v return ctx } func (ctx *RenderContext) WithUseAbsoluteLink(v bool) *RenderContext { ctx.RenderOptions.UseAbsoluteLink = v return ctx } func (ctx *RenderContext) WithHelper(helper RenderHelper) *RenderContext { ctx.RenderHelper = helper return ctx } // FindRendererByContext finds renderer by RenderContext // TODO: it should be merged with other similar functions like GetRendererByFileName, DetectMarkupTypeByFileName, etc func FindRendererByContext(ctx *RenderContext) (Renderer, error) { if ctx.RenderOptions.MarkupType == "" && ctx.RenderOptions.RelativePath != "" { ctx.RenderOptions.MarkupType = DetectMarkupTypeByFileName(ctx.RenderOptions.RelativePath) if ctx.RenderOptions.MarkupType == "" { return nil, util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath) } } renderer := renderers[ctx.RenderOptions.MarkupType] if renderer == nil { return nil, util.NewNotExistErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType) } return renderer, nil } func RendererNeedPostProcess(renderer Renderer) bool { if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() { return true } return false } // Render renders markup file to HTML with all specific handling stuff. func Render(ctx *RenderContext, input io.Reader, output io.Writer) error { renderer, err := FindRendererByContext(ctx) if err != nil { return err } return RenderWithRenderer(ctx, renderer, input, output) } // RenderString renders Markup string to HTML with all specific handling stuff and return string func RenderString(ctx *RenderContext, content string) (string, error) { var buf strings.Builder if err := Render(ctx, strings.NewReader(content), &buf); err != nil { return "", err } return buf.String(), nil } func renderIFrame(ctx *RenderContext, sandbox string, output io.Writer) error { src := fmt.Sprintf("%s/%s/%s/render/%s/%s", setting.AppSubURL, url.PathEscape(ctx.RenderOptions.Metas["user"]), url.PathEscape(ctx.RenderOptions.Metas["repo"]), util.PathEscapeSegments(ctx.RenderOptions.Metas["RefTypeNameSubURL"]), util.PathEscapeSegments(ctx.RenderOptions.RelativePath), ) var sandboxAttrValue template.HTML if sandbox != "" { sandboxAttrValue = htmlutil.HTMLFormat(`sandbox="%s"`, sandbox) } iframe := htmlutil.HTMLFormat(``, src, sandboxAttrValue) _, err := io.WriteString(output, string(iframe)) return err } func pipes() (io.ReadCloser, io.WriteCloser, func()) { pr, pw := io.Pipe() return pr, pw, func() { _ = pr.Close() _ = pw.Close() } } func getExternalRendererOptions(renderer Renderer) (ret ExternalRendererOptions, _ bool) { if externalRender, ok := renderer.(ExternalRenderer); ok { return externalRender.GetExternalRendererOptions(), true } return ret, false } func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error { var extraHeadHTML template.HTML if extOpts, ok := getExternalRendererOptions(renderer); ok && extOpts.DisplayInIframe { if !ctx.RenderOptions.InStandalonePage { // for an external "DisplayInIFrame" render, it could only output its content in a standalone page // otherwise, a