2024-11-04 13:59:50 +03:00
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package markup
import (
"context"
"fmt"
"io"
"net/url"
"strings"
2024-11-22 08:48:09 +03:00
"time"
2024-11-04 13:59:50 +03:00
2024-11-18 08:25:42 +03:00
"code.gitea.io/gitea/modules/markup/internal"
2024-11-04 13:59:50 +03:00
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/yuin/goldmark/ast"
2024-11-18 08:25:42 +03:00
"golang.org/x/sync/errgroup"
2024-11-04 13:59:50 +03:00
)
type RenderMetaMode string
const (
RenderMetaAsDetails RenderMetaMode = "details" // default
RenderMetaAsNone RenderMetaMode = "none"
RenderMetaAsTable RenderMetaMode = "table"
)
2024-11-14 08:02:11 +03:00
var RenderBehaviorForTesting struct {
2024-11-24 11:18:57 +03:00
// Gitea will emit some additional attributes for various purposes, these attributes don't affect rendering.
2024-11-14 08:02:11 +03:00
// 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.
2024-11-24 11:18:57 +03:00
DisableAdditionalAttributes bool
2024-11-14 08:02:11 +03:00
}
2024-11-22 08:48:09 +03:00
type RenderOptions struct {
2024-11-24 11:18:57 +03:00
UseAbsoluteLink bool
2024-11-22 08:48:09 +03:00
// relative path from tree root of the branch
RelativePath string
2024-11-14 08:02:11 +03:00
// eg: "orgmode", "asciicast", "console"
// for file mode, it could be left as empty, and will be detected by file extension in RelativePath
MarkupType string
2024-11-16 11:41:44 +03:00
// user&repo, format&style®exp (for external issue pattern), teams&org (for mention)
// BranchNameSubURL (for iframe&asciicast)
2024-11-24 11:18:57 +03:00
// markupAllowShortIssuePattern
2024-11-16 11:41:44 +03:00
// markdownLineBreakStyle (comment, document)
Metas map [ string ] string
2024-11-14 08:02:11 +03:00
2024-11-22 08:48:09 +03:00
// 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
2024-11-18 08:25:42 +03:00
2024-12-04 04:39:33 +03:00
// the context might be used by the "render" function, but it might also be used by "postProcess" function
usedByRender bool
2024-11-22 08:48:09 +03:00
SidebarTocNode ast . Node
RenderHelper RenderHelper
RenderOptions RenderOptions
2024-11-18 08:25:42 +03:00
RenderInternal internal . RenderInternal
2024-11-04 13:59:50 +03:00
}
2024-11-22 08:48:09 +03:00
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 {
2024-11-24 11:18:57 +03:00
return & RenderContext { ctx : ctx , RenderHelper : & SimpleRenderHelper { } }
2024-11-22 08:48:09 +03:00
}
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
}
2024-11-24 11:18:57 +03:00
func ( ctx * RenderContext ) WithUseAbsoluteLink ( v bool ) * RenderContext {
ctx . RenderOptions . UseAbsoluteLink = v
2024-11-22 08:48:09 +03:00
return ctx
}
2024-11-24 11:18:57 +03:00
func ( ctx * RenderContext ) WithHelper ( helper RenderHelper ) * RenderContext {
ctx . RenderHelper = helper
2024-11-22 08:48:09 +03:00
return ctx
}
2024-11-04 13:59:50 +03:00
// Render renders markup file to HTML with all specific handling stuff.
func Render ( ctx * RenderContext , input io . Reader , output io . Writer ) error {
2024-11-22 08:48:09 +03:00
if ctx . RenderOptions . MarkupType == "" && ctx . RenderOptions . RelativePath != "" {
ctx . RenderOptions . MarkupType = DetectMarkupTypeByFileName ( ctx . RenderOptions . RelativePath )
if ctx . RenderOptions . MarkupType == "" {
return util . NewInvalidArgumentErrorf ( "unsupported file to render: %q" , ctx . RenderOptions . RelativePath )
2024-11-14 08:02:11 +03:00
}
}
2024-11-22 08:48:09 +03:00
renderer := renderers [ ctx . RenderOptions . MarkupType ]
2024-11-14 08:02:11 +03:00
if renderer == nil {
2024-11-22 08:48:09 +03:00
return util . NewInvalidArgumentErrorf ( "unsupported markup type: %q" , ctx . RenderOptions . MarkupType )
2024-11-14 08:02:11 +03:00
}
2024-11-22 08:48:09 +03:00
if ctx . RenderOptions . RelativePath != "" {
2024-11-14 08:02:11 +03:00
if externalRender , ok := renderer . ( ExternalRenderer ) ; ok && externalRender . DisplayInIFrame ( ) {
2024-11-22 08:48:09 +03:00
if ! ctx . RenderOptions . InStandalonePage {
2024-11-14 08:02:11 +03:00
// for an external "DisplayInIFrame" render, it could only output its content in a standalone page
// otherwise, a <iframe> should be outputted to embed the external rendered page
return renderIFrame ( ctx , output )
}
}
2024-11-04 13:59:50 +03:00
}
2024-11-14 08:02:11 +03:00
return render ( ctx , renderer , input , output )
2024-11-04 13:59:50 +03:00
}
// 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 , output io . Writer ) error {
// set height="0" ahead, otherwise the scrollHeight would be max(150, realHeight)
// at the moment, only "allow-scripts" is allowed for sandbox mode.
// "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
// 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
_ , err := io . WriteString ( output , fmt . Sprintf ( `
< iframe src = "%s/%s/%s/render/%s/%s"
name = "giteaExternalRender"
onload = "this.height=giteaExternalRender.document.documentElement.scrollHeight"
width = "100%%" height = "0" scrolling = "no" frameborder = "0" style = "overflow: hidden"
sandbox = "allow-scripts"
> < / iframe > ` ,
setting . AppSubURL ,
2024-11-22 08:48:09 +03:00
url . PathEscape ( ctx . RenderOptions . Metas [ "user" ] ) ,
url . PathEscape ( ctx . RenderOptions . Metas [ "repo" ] ) ,
ctx . RenderOptions . Metas [ "BranchNameSubURL" ] ,
url . PathEscape ( ctx . RenderOptions . RelativePath ) ,
2024-11-04 13:59:50 +03:00
) )
return err
}
2024-11-18 08:25:42 +03:00
func pipes ( ) ( io . ReadCloser , io . WriteCloser , func ( ) ) {
2024-11-04 13:59:50 +03:00
pr , pw := io . Pipe ( )
2024-11-18 08:25:42 +03:00
return pr , pw , func ( ) {
2024-11-04 13:59:50 +03:00
_ = pr . Close ( )
_ = pw . Close ( )
}
2024-11-18 08:25:42 +03:00
}
2024-11-04 13:59:50 +03:00
2024-11-18 08:25:42 +03:00
func render ( ctx * RenderContext , renderer Renderer , input io . Reader , output io . Writer ) error {
2024-12-04 04:39:33 +03:00
ctx . usedByRender = true
2024-11-24 11:18:57 +03:00
if ctx . RenderHelper != nil {
defer ctx . RenderHelper . CleanUp ( )
}
2024-11-18 08:25:42 +03:00
finalProcessor := ctx . RenderInternal . Init ( output )
defer finalProcessor . Close ( )
// input -> (pw1=pr1) -> renderer -> (pw2=pr2) -> SanitizeReader -> finalProcessor -> output
// no sanitizer: input -> (pw1=pr1) -> renderer -> pw2(finalProcessor) -> output
pr1 , pw1 , close1 := pipes ( )
defer close1 ( )
2024-11-22 08:48:09 +03:00
eg , _ := errgroup . WithContext ( ctx )
2024-11-18 08:25:42 +03:00
var pw2 io . WriteCloser = util . NopCloser { Writer : finalProcessor }
if r , ok := renderer . ( ExternalRenderer ) ; ! ok || ! r . SanitizerDisabled ( ) {
var pr2 io . ReadCloser
var close2 func ( )
pr2 , pw2 , close2 = pipes ( )
defer close2 ( )
eg . Go ( func ( ) error {
defer pr2 . Close ( )
return SanitizeReader ( pr2 , renderer . Name ( ) , finalProcessor )
} )
2024-11-04 13:59:50 +03:00
}
2024-11-18 08:25:42 +03:00
eg . Go ( func ( ) ( err error ) {
2024-11-04 13:59:50 +03:00
if r , ok := renderer . ( PostProcessRenderer ) ; ok && r . NeedPostProcess ( ) {
2024-11-26 19:46:02 +03:00
err = PostProcessDefault ( ctx , pr1 , pw2 )
2024-11-04 13:59:50 +03:00
} else {
2024-11-18 08:25:42 +03:00
_ , err = io . Copy ( pw2 , pr1 )
2024-11-04 13:59:50 +03:00
}
2024-11-18 08:25:42 +03:00
_ , _ = pr1 . Close ( ) , pw2 . Close ( )
return err
} )
2024-11-04 13:59:50 +03:00
2024-11-18 08:25:42 +03:00
if err := renderer . Render ( ctx , input , pw1 ) ; err != nil {
return err
2024-11-04 13:59:50 +03:00
}
2024-11-18 08:25:42 +03:00
_ = pw1 . Close ( )
2024-11-04 13:59:50 +03:00
2024-11-18 08:25:42 +03:00
return eg . Wait ( )
2024-11-04 13:59:50 +03:00
}
// Init initializes the render global variables
2024-11-24 11:18:57 +03:00
func Init ( renderHelpFuncs * RenderHelperFuncs ) {
DefaultRenderHelperFuncs = renderHelpFuncs
2024-11-04 13:59:50 +03:00
if len ( setting . Markdown . CustomURLSchemes ) > 0 {
CustomLinkURLSchemes ( setting . Markdown . CustomURLSchemes )
}
// since setting maybe changed extensions, this will reload all renderer extensions mapping
extRenderers = make ( map [ string ] Renderer )
for _ , renderer := range renderers {
for _ , ext := range renderer . Extensions ( ) {
extRenderers [ strings . ToLower ( ext ) ] = renderer
}
}
}
2024-11-16 11:41:44 +03:00
func ComposeSimpleDocumentMetas ( ) map [ string ] string {
return map [ string ] string { "markdownLineBreakStyle" : "document" }
}
2024-11-22 08:48:09 +03:00
2024-11-24 11:18:57 +03:00
type TestRenderHelper struct {
ctx * RenderContext
BaseLink string
}
func ( r * TestRenderHelper ) CleanUp ( ) { }
func ( r * TestRenderHelper ) IsCommitIDExisting ( commitID string ) bool {
return strings . HasPrefix ( commitID , "65f1bf2" ) //|| strings.HasPrefix(commitID, "88fc37a")
}
func ( r * TestRenderHelper ) ResolveLink ( link string , likeType LinkType ) string {
return r . ctx . ResolveLinkRelative ( r . BaseLink , "" , link )
}
var _ RenderHelper = ( * TestRenderHelper ) ( nil )
2024-11-22 08:48:09 +03:00
// NewTestRenderContext is a helper function to create a RenderContext for testing purpose
2024-11-24 11:18:57 +03:00
// It accepts string (BaseLink), map[string]string (Metas)
func NewTestRenderContext ( baseLinkOrMetas ... any ) * RenderContext {
2024-11-22 08:48:09 +03:00
if ! setting . IsInTesting {
panic ( "NewTestRenderContext should only be used in testing" )
}
2024-11-24 11:18:57 +03:00
helper := & TestRenderHelper { }
ctx := NewRenderContext ( context . Background ( ) ) . WithHelper ( helper )
helper . ctx = ctx
for _ , v := range baseLinkOrMetas {
2024-11-22 08:48:09 +03:00
switch v := v . ( type ) {
case string :
2024-11-24 11:18:57 +03:00
helper . BaseLink = v
2024-11-22 08:48:09 +03:00
case map [ string ] string :
ctx = ctx . WithMetas ( v )
default :
panic ( fmt . Sprintf ( "unknown type %T" , v ) )
}
}
return ctx
}