mirror of
https://github.com/go-gitea/gitea
synced 2024-12-26 18:44:27 +00:00
8a20fba8eb
Remove unmaintainable sanitizer rules. No need to add special "class" regexp rules anymore, use RenderInternal.SafeAttr instead, more details (and examples) are in the tests
238 lines
6.8 KiB
Go
238 lines
6.8 KiB
Go
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package markdown
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
|
|
"code.gitea.io/gitea/modules/container"
|
|
"code.gitea.io/gitea/modules/markup"
|
|
"code.gitea.io/gitea/modules/markup/internal"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
|
|
"github.com/yuin/goldmark/ast"
|
|
east "github.com/yuin/goldmark/extension/ast"
|
|
"github.com/yuin/goldmark/parser"
|
|
"github.com/yuin/goldmark/renderer"
|
|
"github.com/yuin/goldmark/renderer/html"
|
|
"github.com/yuin/goldmark/text"
|
|
"github.com/yuin/goldmark/util"
|
|
)
|
|
|
|
// ASTTransformer is a default transformer of the goldmark tree.
|
|
type ASTTransformer struct {
|
|
renderInternal *internal.RenderInternal
|
|
attentionTypes container.Set[string]
|
|
}
|
|
|
|
func NewASTTransformer(renderInternal *internal.RenderInternal) *ASTTransformer {
|
|
return &ASTTransformer{
|
|
renderInternal: renderInternal,
|
|
attentionTypes: container.SetOf("note", "tip", "important", "warning", "caution"),
|
|
}
|
|
}
|
|
|
|
func (g *ASTTransformer) applyElementDir(n ast.Node) {
|
|
if markup.DefaultProcessorHelper.ElementDir != "" {
|
|
n.SetAttributeString("dir", []byte(markup.DefaultProcessorHelper.ElementDir))
|
|
}
|
|
}
|
|
|
|
// Transform transforms the given AST tree.
|
|
func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
|
|
firstChild := node.FirstChild()
|
|
tocMode := ""
|
|
ctx := pc.Get(renderContextKey).(*markup.RenderContext)
|
|
rc := pc.Get(renderConfigKey).(*RenderConfig)
|
|
|
|
tocList := make([]Header, 0, 20)
|
|
if rc.yamlNode != nil {
|
|
metaNode := rc.toMetaNode()
|
|
if metaNode != nil {
|
|
node.InsertBefore(node, firstChild, metaNode)
|
|
}
|
|
tocMode = rc.TOC
|
|
}
|
|
|
|
_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
if !entering {
|
|
return ast.WalkContinue, nil
|
|
}
|
|
|
|
switch v := n.(type) {
|
|
case *ast.Heading:
|
|
g.transformHeading(ctx, v, reader, &tocList)
|
|
case *ast.Paragraph:
|
|
g.applyElementDir(v)
|
|
case *ast.Image:
|
|
g.transformImage(ctx, v)
|
|
case *ast.Link:
|
|
g.transformLink(ctx, v)
|
|
case *ast.List:
|
|
g.transformList(ctx, v, rc)
|
|
case *ast.Text:
|
|
if v.SoftLineBreak() && !v.HardLineBreak() {
|
|
// TODO: this was a quite unclear part, old code: `if metas["mode"] != "document" { use comment link break setting }`
|
|
// many places render non-comment contents with no mode=document, then these contents also use comment's hard line break setting
|
|
// especially in many tests.
|
|
markdownLineBreakStyle := ctx.Metas["markdownLineBreakStyle"]
|
|
if markup.RenderBehaviorForTesting.ForceHardLineBreak {
|
|
v.SetHardLineBreak(true)
|
|
} else if markdownLineBreakStyle == "comment" {
|
|
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments)
|
|
} else if markdownLineBreakStyle == "document" {
|
|
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments)
|
|
}
|
|
}
|
|
case *ast.CodeSpan:
|
|
g.transformCodeSpan(ctx, v, reader)
|
|
case *ast.Blockquote:
|
|
return g.transformBlockquote(v, reader)
|
|
}
|
|
return ast.WalkContinue, nil
|
|
})
|
|
|
|
showTocInMain := tocMode == "true" /* old behavior, in main view */ || tocMode == "main"
|
|
showTocInSidebar := !showTocInMain && tocMode != "false" // not hidden, not main, then show it in sidebar
|
|
if len(tocList) > 0 && (showTocInMain || showTocInSidebar) {
|
|
if showTocInMain {
|
|
tocNode := createTOCNode(tocList, rc.Lang, nil)
|
|
node.InsertBefore(node, firstChild, tocNode)
|
|
} else {
|
|
tocNode := createTOCNode(tocList, rc.Lang, map[string]string{"open": "open"})
|
|
ctx.SidebarTocNode = tocNode
|
|
}
|
|
}
|
|
|
|
if len(rc.Lang) > 0 {
|
|
node.SetAttributeString("lang", []byte(rc.Lang))
|
|
}
|
|
}
|
|
|
|
// it is copied from old code, which is quite doubtful whether it is correct
|
|
var reValidIconName = sync.OnceValue[*regexp.Regexp](func() *regexp.Regexp {
|
|
return regexp.MustCompile(`^[-\w]+$`) // old: regexp.MustCompile("^[a-z ]+$")
|
|
})
|
|
|
|
// NewHTMLRenderer creates a HTMLRenderer to render in the gitea form.
|
|
func NewHTMLRenderer(renderInternal *internal.RenderInternal, opts ...html.Option) renderer.NodeRenderer {
|
|
r := &HTMLRenderer{
|
|
renderInternal: renderInternal,
|
|
Config: html.NewConfig(),
|
|
}
|
|
for _, opt := range opts {
|
|
opt.SetHTMLOption(&r.Config)
|
|
}
|
|
return r
|
|
}
|
|
|
|
// HTMLRenderer is a renderer.NodeRenderer implementation that
|
|
// renders gitea specific features.
|
|
type HTMLRenderer struct {
|
|
html.Config
|
|
renderInternal *internal.RenderInternal
|
|
}
|
|
|
|
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
|
|
func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
|
reg.Register(ast.KindDocument, r.renderDocument)
|
|
reg.Register(KindDetails, r.renderDetails)
|
|
reg.Register(KindSummary, r.renderSummary)
|
|
reg.Register(KindIcon, r.renderIcon)
|
|
reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
|
|
reg.Register(KindAttention, r.renderAttention)
|
|
reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
|
|
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
|
|
}
|
|
|
|
func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
n := node.(*ast.Document)
|
|
|
|
if val, has := n.AttributeString("lang"); has {
|
|
var err error
|
|
if entering {
|
|
_, err = w.WriteString("<div")
|
|
if err == nil {
|
|
_, err = w.WriteString(fmt.Sprintf(` lang=%q`, val))
|
|
}
|
|
if err == nil {
|
|
_, err = w.WriteRune('>')
|
|
}
|
|
} else {
|
|
_, err = w.WriteString("</div>")
|
|
}
|
|
|
|
if err != nil {
|
|
return ast.WalkStop, err
|
|
}
|
|
}
|
|
|
|
return ast.WalkContinue, nil
|
|
}
|
|
|
|
func (r *HTMLRenderer) renderDetails(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
var err error
|
|
if entering {
|
|
if _, err = w.WriteString("<details"); err != nil {
|
|
return ast.WalkStop, err
|
|
}
|
|
html.RenderAttributes(w, node, nil)
|
|
_, err = w.WriteString(">")
|
|
} else {
|
|
_, err = w.WriteString("</details>")
|
|
}
|
|
|
|
if err != nil {
|
|
return ast.WalkStop, err
|
|
}
|
|
|
|
return ast.WalkContinue, nil
|
|
}
|
|
|
|
func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
var err error
|
|
if entering {
|
|
_, err = w.WriteString("<summary>")
|
|
} else {
|
|
_, err = w.WriteString("</summary>")
|
|
}
|
|
|
|
if err != nil {
|
|
return ast.WalkStop, err
|
|
}
|
|
|
|
return ast.WalkContinue, nil
|
|
}
|
|
|
|
func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
if !entering {
|
|
return ast.WalkContinue, nil
|
|
}
|
|
|
|
n := node.(*Icon)
|
|
|
|
name := strings.TrimSpace(strings.ToLower(string(n.Name)))
|
|
|
|
if len(name) == 0 {
|
|
// skip this
|
|
return ast.WalkContinue, nil
|
|
}
|
|
|
|
if !reValidIconName().MatchString(name) {
|
|
// skip this
|
|
return ast.WalkContinue, nil
|
|
}
|
|
|
|
// FIXME: the "icon xxx" is from Fomantic UI, it's really questionable whether it still works correctly
|
|
err := r.renderInternal.FormatWithSafeAttrs(w, `<i class="icon %s"></i>`, name)
|
|
if err != nil {
|
|
return ast.WalkStop, err
|
|
}
|
|
|
|
return ast.WalkContinue, nil
|
|
}
|