// Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package markdown import ( "fmt" "regexp" "strings" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/markup" "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 { attentionTypes container.Set[string] } func NewASTTransformer() *ASTTransformer { return &ASTTransformer{ 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. if markup.RenderBehaviorForTesting.ForceHardLineBreak { v.SetHardLineBreak(true) } else if ctx.ContentMode == markup.RenderContentAsComment { v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments) } else { 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)) } } // NewHTMLRenderer creates a HTMLRenderer to render // in the gitea form. func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { r := &HTMLRenderer{ Config: html.NewConfig(), reValidName: regexp.MustCompile("^[a-z ]+$"), } 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 reValidName *regexp.Regexp } // 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("