Refactor markup package (#32399)
To make the markup package easier to maintain: 1. Split some go files into small files 2. Use a shared util.NopCloser, remove duplicate code 3. Remove unused functions
This commit is contained in:
parent
af28ce59b8
commit
61be51e56b
19 changed files with 1154 additions and 1087 deletions
|
@ -5,161 +5,13 @@ package markup
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
type RenderMetaMode string
|
||||
|
||||
const (
|
||||
RenderMetaAsDetails RenderMetaMode = "details" // default
|
||||
RenderMetaAsNone RenderMetaMode = "none"
|
||||
RenderMetaAsTable RenderMetaMode = "table"
|
||||
)
|
||||
|
||||
type ProcessorHelper struct {
|
||||
IsUsernameMentionable func(ctx context.Context, username string) bool
|
||||
|
||||
ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute
|
||||
|
||||
RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error)
|
||||
}
|
||||
|
||||
var DefaultProcessorHelper ProcessorHelper
|
||||
|
||||
// Init initialize regexps for markdown parsing
|
||||
func Init(ph *ProcessorHelper) {
|
||||
if ph != nil {
|
||||
DefaultProcessorHelper = *ph
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Header holds the data about a header.
|
||||
type Header struct {
|
||||
Level int
|
||||
Text string
|
||||
ID string
|
||||
}
|
||||
|
||||
// RenderContext represents a render context
|
||||
type RenderContext struct {
|
||||
Ctx context.Context
|
||||
RelativePath string // relative path from tree root of the branch
|
||||
Type string
|
||||
IsWiki bool
|
||||
Links Links
|
||||
Metas map[string]string // user, repo, mode(comment/document)
|
||||
DefaultLink string
|
||||
GitRepo *git.Repository
|
||||
Repo gitrepo.Repository
|
||||
ShaExistCache map[string]bool
|
||||
cancelFn func()
|
||||
SidebarTocNode ast.Node
|
||||
RenderMetaAs RenderMetaMode
|
||||
InStandalonePage bool // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
|
||||
}
|
||||
|
||||
type Links struct {
|
||||
AbsolutePrefix bool // add absolute URL prefix to auto-resolved links like "#issue", but not for pre-provided links and medias
|
||||
Base string // base prefix for pre-provided links and medias (images, videos)
|
||||
BranchPath string // actually it is the ref path, eg: "branch/features/feat-12", "tag/v1.0"
|
||||
TreePath string // the dir of the file, eg: "doc" if the file "doc/CHANGE.md" is being rendered
|
||||
}
|
||||
|
||||
func (l *Links) Prefix() string {
|
||||
if l.AbsolutePrefix {
|
||||
return setting.AppURL
|
||||
}
|
||||
return setting.AppSubURL
|
||||
}
|
||||
|
||||
func (l *Links) HasBranchInfo() bool {
|
||||
return l.BranchPath != ""
|
||||
}
|
||||
|
||||
func (l *Links) SrcLink() string {
|
||||
return util.URLJoin(l.Base, "src", l.BranchPath, l.TreePath)
|
||||
}
|
||||
|
||||
func (l *Links) MediaLink() string {
|
||||
return util.URLJoin(l.Base, "media", l.BranchPath, l.TreePath)
|
||||
}
|
||||
|
||||
func (l *Links) RawLink() string {
|
||||
return util.URLJoin(l.Base, "raw", l.BranchPath, l.TreePath)
|
||||
}
|
||||
|
||||
func (l *Links) WikiLink() string {
|
||||
return util.URLJoin(l.Base, "wiki")
|
||||
}
|
||||
|
||||
func (l *Links) WikiRawLink() string {
|
||||
return util.URLJoin(l.Base, "wiki/raw")
|
||||
}
|
||||
|
||||
func (l *Links) ResolveMediaLink(isWiki bool) string {
|
||||
if isWiki {
|
||||
return l.WikiRawLink()
|
||||
} else if l.HasBranchInfo() {
|
||||
return l.MediaLink()
|
||||
}
|
||||
return l.Base
|
||||
}
|
||||
|
||||
// Cancel runs any cleanup functions that have been registered for this Ctx
|
||||
func (ctx *RenderContext) Cancel() {
|
||||
if ctx == nil {
|
||||
return
|
||||
}
|
||||
ctx.ShaExistCache = map[string]bool{}
|
||||
if ctx.cancelFn == nil {
|
||||
return
|
||||
}
|
||||
ctx.cancelFn()
|
||||
}
|
||||
|
||||
// AddCancel adds the provided fn as a Cleanup for this Ctx
|
||||
func (ctx *RenderContext) AddCancel(fn func()) {
|
||||
if ctx == nil {
|
||||
return
|
||||
}
|
||||
oldCancelFn := ctx.cancelFn
|
||||
if oldCancelFn == nil {
|
||||
ctx.cancelFn = fn
|
||||
return
|
||||
}
|
||||
ctx.cancelFn = func() {
|
||||
defer oldCancelFn()
|
||||
fn()
|
||||
}
|
||||
}
|
||||
|
||||
// Renderer defines an interface for rendering markup file to HTML
|
||||
type Renderer interface {
|
||||
Name() string // markup format name
|
||||
|
@ -173,7 +25,7 @@ type PostProcessRenderer interface {
|
|||
NeedPostProcess() bool
|
||||
}
|
||||
|
||||
// PostProcessRenderer defines an interface for external renderers
|
||||
// ExternalRenderer defines an interface for external renderers
|
||||
type ExternalRenderer interface {
|
||||
// SanitizerDisabled disabled sanitize if return true
|
||||
SanitizerDisabled() bool
|
||||
|
@ -207,11 +59,6 @@ func GetRendererByFileName(filename string) Renderer {
|
|||
return extRenderers[extension]
|
||||
}
|
||||
|
||||
// GetRendererByType returns a renderer according type
|
||||
func GetRendererByType(tp string) Renderer {
|
||||
return renderers[tp]
|
||||
}
|
||||
|
||||
// DetectRendererType detects the markup type of the content
|
||||
func DetectRendererType(filename string, input io.Reader) string {
|
||||
buf, err := io.ReadAll(input)
|
||||
|
@ -226,152 +73,6 @@ func DetectRendererType(filename string, input io.Reader) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// Render renders markup file to HTML with all specific handling stuff.
|
||||
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
||||
if ctx.Type != "" {
|
||||
return renderByType(ctx, input, output)
|
||||
} else if ctx.RelativePath != "" {
|
||||
return renderFile(ctx, input, output)
|
||||
}
|
||||
return errors.New("Render options both filename and type missing")
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
type nopCloser struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (nopCloser) Close() error { return 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,
|
||||
url.PathEscape(ctx.Metas["user"]),
|
||||
url.PathEscape(ctx.Metas["repo"]),
|
||||
ctx.Metas["BranchNameSubURL"],
|
||||
url.PathEscape(ctx.RelativePath),
|
||||
))
|
||||
return err
|
||||
}
|
||||
|
||||
func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
|
||||
var wg sync.WaitGroup
|
||||
var err error
|
||||
pr, pw := io.Pipe()
|
||||
defer func() {
|
||||
_ = pr.Close()
|
||||
_ = pw.Close()
|
||||
}()
|
||||
|
||||
var pr2 io.ReadCloser
|
||||
var pw2 io.WriteCloser
|
||||
|
||||
var sanitizerDisabled bool
|
||||
if r, ok := renderer.(ExternalRenderer); ok {
|
||||
sanitizerDisabled = r.SanitizerDisabled()
|
||||
}
|
||||
|
||||
if !sanitizerDisabled {
|
||||
pr2, pw2 = io.Pipe()
|
||||
defer func() {
|
||||
_ = pr2.Close()
|
||||
_ = pw2.Close()
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
err = SanitizeReader(pr2, renderer.Name(), output)
|
||||
_ = pr2.Close()
|
||||
wg.Done()
|
||||
}()
|
||||
} else {
|
||||
pw2 = nopCloser{output}
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
|
||||
err = PostProcess(ctx, pr, pw2)
|
||||
} else {
|
||||
_, err = io.Copy(pw2, pr)
|
||||
}
|
||||
_ = pr.Close()
|
||||
_ = pw2.Close()
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
if err1 := renderer.Render(ctx, input, pw); err1 != nil {
|
||||
return err1
|
||||
}
|
||||
_ = pw.Close()
|
||||
|
||||
wg.Wait()
|
||||
return err
|
||||
}
|
||||
|
||||
// ErrUnsupportedRenderType represents
|
||||
type ErrUnsupportedRenderType struct {
|
||||
Type string
|
||||
}
|
||||
|
||||
func (err ErrUnsupportedRenderType) Error() string {
|
||||
return fmt.Sprintf("Unsupported render type: %s", err.Type)
|
||||
}
|
||||
|
||||
func renderByType(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
||||
if renderer, ok := renderers[ctx.Type]; ok {
|
||||
return render(ctx, renderer, input, output)
|
||||
}
|
||||
return ErrUnsupportedRenderType{ctx.Type}
|
||||
}
|
||||
|
||||
// ErrUnsupportedRenderExtension represents the error when extension doesn't supported to render
|
||||
type ErrUnsupportedRenderExtension struct {
|
||||
Extension string
|
||||
}
|
||||
|
||||
func IsErrUnsupportedRenderExtension(err error) bool {
|
||||
_, ok := err.(ErrUnsupportedRenderExtension)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUnsupportedRenderExtension) Error() string {
|
||||
return fmt.Sprintf("Unsupported render extension: %s", err.Extension)
|
||||
}
|
||||
|
||||
func renderFile(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
||||
extension := strings.ToLower(filepath.Ext(ctx.RelativePath))
|
||||
if renderer, ok := extRenderers[extension]; ok {
|
||||
if r, ok := renderer.(ExternalRenderer); ok && r.DisplayInIFrame() {
|
||||
if !ctx.InStandalonePage {
|
||||
// for an external 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)
|
||||
}
|
||||
}
|
||||
return render(ctx, renderer, input, output)
|
||||
}
|
||||
return ErrUnsupportedRenderExtension{extension}
|
||||
}
|
||||
|
||||
// DetectMarkupTypeByFileName returns the possible markup format type via the filename
|
||||
func DetectMarkupTypeByFileName(filename string) string {
|
||||
if parser := GetRendererByFileName(filename); parser != nil {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue