diff --git a/modules/markup/markdown/ast.go b/modules/markup/markdown/ast.go
index 77ce5cb359..72d32600f5 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 0000000000..a5b1bbc2a0
--- /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 0000000000..58f1fc960f
--- /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 36ce6397f4..da2224ce0c 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 771162b9a3..92c0e786e9 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),
),
)