diff --git a/modules/markup/markdown/ast.go b/modules/markup/markdown/ast.go index 77ce5cb35..72d32600f 100644 --- a/modules/markup/markdown/ast.go +++ b/modules/markup/markdown/ast.go @@ -181,32 +181,3 @@ func IsColorPreview(node ast.Node) bool { _, ok := node.(*ColorPreview) return ok } - -// Attention is an inline for an attention -type Attention struct { - ast.BaseInline - AttentionType string -} - -// Dump implements Node.Dump. -func (n *Attention) Dump(source []byte, level int) { - m := map[string]string{} - m["AttentionType"] = n.AttentionType - ast.DumpHelper(n, source, level, m, nil) -} - -// KindAttention is the NodeKind for Attention -var KindAttention = ast.NewNodeKind("Attention") - -// Kind implements Node.Kind. -func (n *Attention) Kind() ast.NodeKind { - return KindAttention -} - -// NewAttention returns a new Attention node. -func NewAttention(attentionType string) *Attention { - return &Attention{ - BaseInline: ast.BaseInline{}, - AttentionType: attentionType, - } -} diff --git a/modules/markup/markdown/callout/ast.go b/modules/markup/markdown/callout/ast.go new file mode 100644 index 000000000..a5b1bbc2a --- /dev/null +++ b/modules/markup/markdown/callout/ast.go @@ -0,0 +1,37 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package callout + +import ( + "github.com/yuin/goldmark/ast" +) + +// Attention is an inline for an attention +type Attention struct { + ast.BaseInline + AttentionType string +} + +// Dump implements Node.Dump. +func (n *Attention) Dump(source []byte, level int) { + m := map[string]string{} + m["AttentionType"] = n.AttentionType + ast.DumpHelper(n, source, level, m, nil) +} + +// KindAttention is the NodeKind for Attention +var KindAttention = ast.NewNodeKind("Attention") + +// Kind implements Node.Kind. +func (n *Attention) Kind() ast.NodeKind { + return KindAttention +} + +// NewAttention returns a new Attention node. +func NewAttention(attentionType string) *Attention { + return &Attention{ + BaseInline: ast.BaseInline{}, + AttentionType: attentionType, + } +} diff --git a/modules/markup/markdown/callout/github.go b/modules/markup/markdown/callout/github.go new file mode 100644 index 000000000..58f1fc960 --- /dev/null +++ b/modules/markup/markdown/callout/github.go @@ -0,0 +1,142 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package callout + +import ( + "strings" + + "code.gitea.io/gitea/modules/svg" + + "github.com/yuin/goldmark/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" +) + +type GitHubCalloutTransformer struct{} + +// Transform transforms the given AST tree. +func (g *GitHubCalloutTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { + supportedAttentionTypes := map[string]bool{ + "note": true, + "tip": true, + "important": true, + "warning": true, + "caution": true, + } + + _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + + switch v := n.(type) { + case *ast.Blockquote: + // We only want attention blockquotes when the AST looks like: + // Text: "[" + // Text: "!TYPE" + // Text(SoftLineBreak): "]" + + // grab these nodes and make sure we adhere to the attention blockquote structure + firstParagraph := v.FirstChild() + if firstParagraph.ChildCount() < 3 { + return ast.WalkContinue, nil + } + firstTextNode, ok := firstParagraph.FirstChild().(*ast.Text) + if !ok || string(firstTextNode.Text(reader.Source())) != "[" { + return ast.WalkContinue, nil + } + secondTextNode, ok := firstTextNode.NextSibling().(*ast.Text) + if !ok { + return ast.WalkContinue, nil + } + // If the second node's text isn't one of the supported attention + // types, continue walking. + secondTextNodeText := secondTextNode.Text(reader.Source()) + attentionType := strings.ToLower(strings.TrimPrefix(string(secondTextNodeText), "!")) + if _, has := supportedAttentionTypes[attentionType]; !has { + return ast.WalkContinue, nil + } + + thirdTextNode, ok := secondTextNode.NextSibling().(*ast.Text) + if !ok || string(thirdTextNode.Text(reader.Source())) != "]" { + return ast.WalkContinue, nil + } + + // color the blockquote + v.SetAttributeString("class", []byte("gt-py-3 attention attention-"+attentionType)) + + // create an emphasis to make it bold + emphasis := ast.NewEmphasis(2) + emphasis.SetAttributeString("class", []byte("attention-"+attentionType)) + firstParagraph.InsertBefore(firstParagraph, firstTextNode, emphasis) + + // capitalize first letter + attentionText := ast.NewString([]byte(strings.ToUpper(string(attentionType[0])) + attentionType[1:])) + + // replace the ![TYPE] with icon+Type + emphasis.AppendChild(emphasis, attentionText) + for i := 0; i < 2; i++ { + lineBreak := ast.NewText() + lineBreak.SetSoftLineBreak(true) + firstParagraph.InsertAfter(firstParagraph, emphasis, lineBreak) + } + firstParagraph.InsertBefore(firstParagraph, emphasis, NewAttention(attentionType)) + firstParagraph.RemoveChild(firstParagraph, firstTextNode) + firstParagraph.RemoveChild(firstParagraph, secondTextNode) + firstParagraph.RemoveChild(firstParagraph, thirdTextNode) + } + return ast.WalkContinue, nil + }) +} + +type GitHubCalloutHTMLRenderer struct { + html.Config +} + +// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. +func (r *GitHubCalloutHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(KindAttention, r.renderAttention) +} + +// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg +func (r *GitHubCalloutHTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + if entering { + _, _ = w.WriteString(``) + + var octiconType string + switch n.AttentionType { + case "note": + octiconType = "info" + case "tip": + octiconType = "light-bulb" + case "important": + octiconType = "report" + case "warning": + octiconType = "alert" + case "caution": + octiconType = "stop" + } + _, _ = w.WriteString(string(svg.RenderHTML("octicon-" + octiconType))) + } else { + _, _ = w.WriteString("\n") + } + return ast.WalkContinue, nil +} + +func NewGitHubCalloutHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { + r := &GitHubCalloutHTMLRenderer{ + Config: html.NewConfig(), + } + for _, opt := range opts { + opt.SetHTMLOption(&r.Config) + } + return r +} diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 36ce6397f..da2224ce0 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -13,7 +13,6 @@ import ( "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/common" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/svg" giteautil "code.gitea.io/gitea/modules/util" "github.com/microcosm-cc/bluemonday/css" @@ -196,55 +195,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa if css.ColorHandler(strings.ToLower(string(colorContent))) { v.AppendChild(v, NewColorPreview(colorContent)) } - case *ast.Blockquote: - // We only want attention blockquotes when the AST looks like: - // Text: "[" - // Text: "!TYPE" - // Text(SoftLineBreak): "]" - - // grab these nodes and make sure we adhere to the attention blockquote structure - firstParagraph := v.FirstChild() - if firstParagraph.ChildCount() < 3 { - return ast.WalkContinue, nil - } - firstTextNode, ok := firstParagraph.FirstChild().(*ast.Text) - if !ok || string(firstTextNode.Segment.Value(reader.Source())) != "[" { - return ast.WalkContinue, nil - } - secondTextNode, ok := firstTextNode.NextSibling().(*ast.Text) - if !ok || !attentionTypeRE.MatchString(string(secondTextNode.Segment.Value(reader.Source()))) { - return ast.WalkContinue, nil - } - thirdTextNode, ok := secondTextNode.NextSibling().(*ast.Text) - if !ok || string(thirdTextNode.Segment.Value(reader.Source())) != "]" { - return ast.WalkContinue, nil - } - - // grab attention type from markdown source - attentionType := strings.ToLower(strings.TrimPrefix(string(secondTextNode.Segment.Value(reader.Source())), "!")) - - // color the blockquote - v.SetAttributeString("class", []byte("gt-py-3 attention attention-"+attentionType)) - - // create an emphasis to make it bold - emphasis := ast.NewEmphasis(2) - emphasis.SetAttributeString("class", []byte("attention-"+attentionType)) - firstParagraph.InsertBefore(firstParagraph, firstTextNode, emphasis) - - // capitalize first letter - attentionText := ast.NewString([]byte(strings.ToUpper(string(attentionType[0])) + attentionType[1:])) - - // replace the ![TYPE] with icon+Type - emphasis.AppendChild(emphasis, attentionText) - for i := 0; i < 2; i++ { - lineBreak := ast.NewText() - lineBreak.SetSoftLineBreak(true) - firstParagraph.InsertAfter(firstParagraph, emphasis, lineBreak) - } - firstParagraph.InsertBefore(firstParagraph, emphasis, NewAttention(attentionType)) - firstParagraph.RemoveChild(firstParagraph, firstTextNode) - firstParagraph.RemoveChild(firstParagraph, secondTextNode) - firstParagraph.RemoveChild(firstParagraph, thirdTextNode) } return ast.WalkContinue, nil }) @@ -335,7 +285,6 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 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) } @@ -372,34 +321,6 @@ func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Nod return ast.WalkContinue, nil } -// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg -func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { - if entering { - _, _ = w.WriteString(``) - - var octiconType string - switch n.AttentionType { - case "note": - octiconType = "info" - case "tip": - octiconType = "light-bulb" - case "important": - octiconType = "report" - case "warning": - octiconType = "alert" - case "caution": - octiconType = "stop" - } - _, _ = w.WriteString(string(svg.RenderHTML("octicon-" + octiconType))) - } else { - _, _ = w.WriteString("\n") - } - return ast.WalkContinue, nil -} - func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*ast.Document) @@ -459,10 +380,7 @@ func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.N return ast.WalkContinue, nil } -var ( - validNameRE = regexp.MustCompile("^[a-z ]+$") - attentionTypeRE = regexp.MustCompile("^!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)$") -) +var validNameRE = regexp.MustCompile("^[a-z ]+$") func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { if !entering { diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index 771162b9a..92c0e786e 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/common" + "code.gitea.io/gitea/modules/markup/markdown/callout" "code.gitea.io/gitea/modules/markup/markdown/math" "code.gitea.io/gitea/modules/setting" giteautil "code.gitea.io/gitea/modules/util" @@ -124,6 +125,7 @@ func SpecializedMarkdown() goldmark.Markdown { parser.WithAttribute(), parser.WithAutoHeadingID(), parser.WithASTTransformers( + util.Prioritized(&callout.GitHubCalloutTransformer{}, 9000), util.Prioritized(&ASTTransformer{}, 10000), ), ), @@ -135,6 +137,7 @@ func SpecializedMarkdown() goldmark.Markdown { // Override the original Tasklist renderer! specMarkdown.Renderer().AddOptions( renderer.WithNodeRenderers( + util.Prioritized(callout.NewGitHubCalloutHTMLRenderer(), 10), util.Prioritized(NewHTMLRenderer(), 10), ), )