2019-12-31 02:53:28 +01:00
|
|
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
2022-11-27 19:20:29 +01:00
|
|
|
// SPDX-License-Identifier: MIT
|
2019-12-31 02:53:28 +01:00
|
|
|
|
|
|
|
package markdown
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2020-04-24 15:22:36 +02:00
|
|
|
"regexp"
|
2019-12-31 02:53:28 +01:00
|
|
|
"strings"
|
|
|
|
|
2022-10-12 07:18:26 +02:00
|
|
|
"code.gitea.io/gitea/modules/container"
|
2019-12-31 02:53:28 +01:00
|
|
|
"code.gitea.io/gitea/modules/markup"
|
2020-04-24 15:22:36 +02:00
|
|
|
"code.gitea.io/gitea/modules/setting"
|
2019-12-31 02:53:28 +01:00
|
|
|
|
|
|
|
"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"
|
|
|
|
)
|
|
|
|
|
2020-04-24 15:22:36 +02:00
|
|
|
// ASTTransformer is a default transformer of the goldmark tree.
|
2024-03-22 13:16:23 +01:00
|
|
|
type ASTTransformer struct {
|
2024-03-28 03:26:13 +01:00
|
|
|
attentionTypes container.Set[string]
|
2024-03-22 13:16:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewASTTransformer() *ASTTransformer {
|
|
|
|
return &ASTTransformer{
|
2024-03-28 03:26:13 +01:00
|
|
|
attentionTypes: container.SetOf("note", "tip", "important", "warning", "caution"),
|
2024-03-22 13:16:23 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (g *ASTTransformer) applyElementDir(n ast.Node) {
|
|
|
|
if markup.DefaultProcessorHelper.ElementDir != "" {
|
|
|
|
n.SetAttributeString("dir", []byte(markup.DefaultProcessorHelper.ElementDir))
|
|
|
|
}
|
|
|
|
}
|
2019-12-31 02:53:28 +01:00
|
|
|
|
|
|
|
// Transform transforms the given AST tree.
|
2020-04-24 15:22:36 +02:00
|
|
|
func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
|
|
|
|
firstChild := node.FirstChild()
|
2023-04-17 21:05:19 +02:00
|
|
|
tocMode := ""
|
2022-06-08 10:59:16 +02:00
|
|
|
ctx := pc.Get(renderContextKey).(*markup.RenderContext)
|
2022-09-13 18:33:37 +02:00
|
|
|
rc := pc.Get(renderConfigKey).(*RenderConfig)
|
2023-04-17 21:05:19 +02:00
|
|
|
|
|
|
|
tocList := make([]markup.Header, 0, 20)
|
2022-09-13 18:33:37 +02:00
|
|
|
if rc.yamlNode != nil {
|
|
|
|
metaNode := rc.toMetaNode()
|
2020-04-24 15:22:36 +02:00
|
|
|
if metaNode != nil {
|
|
|
|
node.InsertBefore(node, firstChild, metaNode)
|
|
|
|
}
|
2023-04-17 21:05:19 +02:00
|
|
|
tocMode = rc.TOC
|
2020-04-24 15:22:36 +02:00
|
|
|
}
|
|
|
|
|
2019-12-31 02:53:28 +01:00
|
|
|
_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
|
|
if !entering {
|
|
|
|
return ast.WalkContinue, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
switch v := n.(type) {
|
2020-04-24 15:22:36 +02:00
|
|
|
case *ast.Heading:
|
2024-03-28 03:26:13 +01:00
|
|
|
g.transformHeading(ctx, v, reader, &tocList)
|
2023-05-20 23:02:52 +02:00
|
|
|
case *ast.Paragraph:
|
2024-03-22 13:16:23 +01:00
|
|
|
g.applyElementDir(v)
|
2019-12-31 02:53:28 +01:00
|
|
|
case *ast.Image:
|
2024-04-29 10:47:56 +02:00
|
|
|
g.transformImage(ctx, v)
|
2019-12-31 02:53:28 +01:00
|
|
|
case *ast.Link:
|
2024-04-29 10:47:56 +02:00
|
|
|
g.transformLink(ctx, v)
|
2020-03-22 23:25:38 +01:00
|
|
|
case *ast.List:
|
2024-04-29 10:47:56 +02:00
|
|
|
g.transformList(ctx, v, rc)
|
2020-05-24 10:14:26 +02:00
|
|
|
case *ast.Text:
|
|
|
|
if v.SoftLineBreak() && !v.HardLineBreak() {
|
2024-01-15 09:49:24 +01:00
|
|
|
if ctx.Metas["mode"] != "document" {
|
2020-05-24 10:14:26 +02:00
|
|
|
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments)
|
|
|
|
} else {
|
|
|
|
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments)
|
|
|
|
}
|
|
|
|
}
|
2022-10-21 14:00:53 +02:00
|
|
|
case *ast.CodeSpan:
|
2024-03-28 03:26:13 +01:00
|
|
|
g.transformCodeSpan(ctx, v, reader)
|
2024-02-10 19:43:09 +01:00
|
|
|
case *ast.Blockquote:
|
2024-03-22 13:16:23 +01:00
|
|
|
return g.transformBlockquote(v, reader)
|
2019-12-31 02:53:28 +01:00
|
|
|
}
|
|
|
|
return ast.WalkContinue, nil
|
|
|
|
})
|
2020-04-24 15:22:36 +02:00
|
|
|
|
2023-04-17 21:05:19 +02:00
|
|
|
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)
|
2020-04-24 15:22:36 +02:00
|
|
|
node.InsertBefore(node, firstChild, tocNode)
|
2023-04-17 21:05:19 +02:00
|
|
|
} else {
|
|
|
|
tocNode := createTOCNode(tocList, rc.Lang, map[string]string{"open": "open"})
|
|
|
|
ctx.SidebarTocNode = tocNode
|
2020-04-24 15:22:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(rc.Lang) > 0 {
|
|
|
|
node.SetAttributeString("lang", []byte(rc.Lang))
|
|
|
|
}
|
2019-12-31 02:53:28 +01:00
|
|
|
}
|
|
|
|
|
2020-04-24 15:22:36 +02:00
|
|
|
// NewHTMLRenderer creates a HTMLRenderer to render
|
2019-12-31 02:53:28 +01:00
|
|
|
// in the gitea form.
|
2020-04-24 15:22:36 +02:00
|
|
|
func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
|
|
|
|
r := &HTMLRenderer{
|
2024-03-22 13:16:23 +01:00
|
|
|
Config: html.NewConfig(),
|
|
|
|
reValidName: regexp.MustCompile("^[a-z ]+$"),
|
2019-12-31 02:53:28 +01:00
|
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
|
|
opt.SetHTMLOption(&r.Config)
|
|
|
|
}
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2020-04-24 15:22:36 +02:00
|
|
|
// HTMLRenderer is a renderer.NodeRenderer implementation that
|
|
|
|
// renders gitea specific features.
|
|
|
|
type HTMLRenderer struct {
|
2019-12-31 02:53:28 +01:00
|
|
|
html.Config
|
2024-03-22 13:16:23 +01:00
|
|
|
reValidName *regexp.Regexp
|
2019-12-31 02:53:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
|
2020-04-24 15:22:36 +02:00
|
|
|
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)
|
2022-10-21 14:00:53 +02:00
|
|
|
reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
|
2022-11-09 01:11:26 +01:00
|
|
|
reg.Register(KindAttention, r.renderAttention)
|
2020-04-26 07:09:08 +02:00
|
|
|
reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
|
2019-12-31 02:53:28 +01:00
|
|
|
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
|
|
|
|
}
|
|
|
|
|
2020-04-24 15:22:36 +02:00
|
|
|
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 {
|
2023-04-17 21:05:19 +02:00
|
|
|
if _, err = w.WriteString("<details"); err != nil {
|
|
|
|
return ast.WalkStop, err
|
|
|
|
}
|
|
|
|
html.RenderAttributes(w, node, nil)
|
|
|
|
_, err = w.WriteString(">")
|
2020-04-24 15:22:36 +02:00
|
|
|
} 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
|
|
|
|
}
|
|
|
|
|
2024-03-22 13:16:23 +01:00
|
|
|
if !r.reValidName.MatchString(name) {
|
2020-04-24 15:22:36 +02:00
|
|
|
// skip this
|
|
|
|
return ast.WalkContinue, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
|
|
|
_, err = w.WriteString(fmt.Sprintf(`<i class="icon %s"></i>`, name))
|
|
|
|
if err != nil {
|
|
|
|
return ast.WalkStop, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return ast.WalkContinue, nil
|
|
|
|
}
|