Merge pull request 'GitHub-style alert blocks in Markdown (gitea#29121)' (#2348) from algernon/forgejo:f/markdown/alert-blocks-port into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2348 Reviewed-by: Gusted <gusted@noreply.codeberg.org> Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
		
				commit
				
					
						5240e27266
					
				
			
		
					 9 changed files with 350 additions and 80 deletions
				
			
		| 
						 | 
					@ -181,37 +181,3 @@ func IsColorPreview(node ast.Node) bool {
 | 
				
			||||||
	_, ok := node.(*ColorPreview)
 | 
						_, ok := node.(*ColorPreview)
 | 
				
			||||||
	return ok
 | 
						return ok
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					 | 
				
			||||||
	AttentionNote    string = "Note"
 | 
					 | 
				
			||||||
	AttentionWarning string = "Warning"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Attention is an inline for a color preview
 | 
					 | 
				
			||||||
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,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										37
									
								
								modules/markup/markdown/callout/ast.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								modules/markup/markdown/callout/ast.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										142
									
								
								modules/markup/markdown/callout/github.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								modules/markup/markdown/callout/github.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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(`<span class="gt-mr-2 gt-vm attention-`)
 | 
				
			||||||
 | 
							n := node.(*Attention)
 | 
				
			||||||
 | 
							_, _ = w.WriteString(strings.ToLower(n.AttentionType))
 | 
				
			||||||
 | 
							_, _ = 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("</span>\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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										60
									
								
								modules/markup/markdown/callout/github_legacy.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								modules/markup/markdown/callout/github_legacy.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,60 @@
 | 
				
			||||||
 | 
					// 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"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/yuin/goldmark/ast"
 | 
				
			||||||
 | 
						"github.com/yuin/goldmark/parser"
 | 
				
			||||||
 | 
						"github.com/yuin/goldmark/text"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Transformer for GitHub's legacy callout markup.
 | 
				
			||||||
 | 
					type GitHubLegacyCalloutTransformer struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (g *GitHubLegacyCalloutTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
 | 
				
			||||||
 | 
						supportedCalloutTypes := map[string]bool{"Note": true, "Warning": 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:
 | 
				
			||||||
 | 
								// The first paragraph contains the callout type.
 | 
				
			||||||
 | 
								firstParagraph := v.FirstChild()
 | 
				
			||||||
 | 
								if firstParagraph.ChildCount() < 1 {
 | 
				
			||||||
 | 
									return ast.WalkContinue, nil
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// In the legacy GitHub callout markup, the first node of the first
 | 
				
			||||||
 | 
								// paragraph should be an emphasis.
 | 
				
			||||||
 | 
								calloutNode, ok := firstParagraph.FirstChild().(*ast.Emphasis)
 | 
				
			||||||
 | 
								if !ok {
 | 
				
			||||||
 | 
									return ast.WalkContinue, nil
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								calloutText := string(calloutNode.Text(reader.Source()))
 | 
				
			||||||
 | 
								calloutType := strings.ToLower(calloutText)
 | 
				
			||||||
 | 
								// We only support "Note" and "Warning" callouts in legacy mode,
 | 
				
			||||||
 | 
								// match only those.
 | 
				
			||||||
 | 
								if _, has := supportedCalloutTypes[calloutText]; !has {
 | 
				
			||||||
 | 
									return ast.WalkContinue, nil
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Set the attention attribute on the emphasis
 | 
				
			||||||
 | 
								calloutNode.SetAttributeString("class", []byte("attention-"+calloutType))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// color the blockquote
 | 
				
			||||||
 | 
								v.SetAttributeString("class", []byte("gt-py-3 attention attention-"+calloutType))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Prepend callout icon before the callout node itself
 | 
				
			||||||
 | 
								firstParagraph.InsertBefore(firstParagraph, calloutNode, NewAttention(calloutType))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return ast.WalkContinue, nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -13,7 +13,6 @@ import (
 | 
				
			||||||
	"code.gitea.io/gitea/modules/markup"
 | 
						"code.gitea.io/gitea/modules/markup"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/markup/common"
 | 
						"code.gitea.io/gitea/modules/markup/common"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/svg"
 | 
					 | 
				
			||||||
	giteautil "code.gitea.io/gitea/modules/util"
 | 
						giteautil "code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/microcosm-cc/bluemonday/css"
 | 
						"github.com/microcosm-cc/bluemonday/css"
 | 
				
			||||||
| 
						 | 
					@ -53,7 +52,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	attentionMarkedBlockquotes := make(container.Set[*ast.Blockquote])
 | 
					 | 
				
			||||||
	_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
 | 
						_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
 | 
				
			||||||
		if !entering {
 | 
							if !entering {
 | 
				
			||||||
			return ast.WalkContinue, nil
 | 
								return ast.WalkContinue, nil
 | 
				
			||||||
| 
						 | 
					@ -197,18 +195,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 | 
				
			||||||
			if css.ColorHandler(strings.ToLower(string(colorContent))) {
 | 
								if css.ColorHandler(strings.ToLower(string(colorContent))) {
 | 
				
			||||||
				v.AppendChild(v, NewColorPreview(colorContent))
 | 
									v.AppendChild(v, NewColorPreview(colorContent))
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		case *ast.Emphasis:
 | 
					 | 
				
			||||||
			// check if inside blockquote for attention, expected hierarchy is
 | 
					 | 
				
			||||||
			// Emphasis < Paragraph < Blockquote
 | 
					 | 
				
			||||||
			blockquote, isInBlockquote := n.Parent().Parent().(*ast.Blockquote)
 | 
					 | 
				
			||||||
			if isInBlockquote && !attentionMarkedBlockquotes.Contains(blockquote) {
 | 
					 | 
				
			||||||
				fullText := string(n.Text(reader.Source()))
 | 
					 | 
				
			||||||
				if fullText == AttentionNote || fullText == AttentionWarning {
 | 
					 | 
				
			||||||
					v.SetAttributeString("class", []byte("attention-"+strings.ToLower(fullText)))
 | 
					 | 
				
			||||||
					v.Parent().InsertBefore(v.Parent(), v, NewAttention(fullText))
 | 
					 | 
				
			||||||
					attentionMarkedBlockquotes.Add(blockquote)
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return ast.WalkContinue, nil
 | 
							return ast.WalkContinue, nil
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
| 
						 | 
					@ -299,7 +285,6 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
 | 
				
			||||||
	reg.Register(KindSummary, r.renderSummary)
 | 
						reg.Register(KindSummary, r.renderSummary)
 | 
				
			||||||
	reg.Register(KindIcon, r.renderIcon)
 | 
						reg.Register(KindIcon, r.renderIcon)
 | 
				
			||||||
	reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
 | 
						reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
 | 
				
			||||||
	reg.Register(KindAttention, r.renderAttention)
 | 
					 | 
				
			||||||
	reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
 | 
						reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
 | 
				
			||||||
	reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
 | 
						reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -336,28 +321,6 @@ func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Nod
 | 
				
			||||||
	return ast.WalkContinue, nil
 | 
						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(`<span class="attention-icon attention-`)
 | 
					 | 
				
			||||||
		n := node.(*Attention)
 | 
					 | 
				
			||||||
		_, _ = w.WriteString(strings.ToLower(n.AttentionType))
 | 
					 | 
				
			||||||
		_, _ = w.WriteString(`">`)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		var octiconType string
 | 
					 | 
				
			||||||
		switch n.AttentionType {
 | 
					 | 
				
			||||||
		case AttentionNote:
 | 
					 | 
				
			||||||
			octiconType = "info"
 | 
					 | 
				
			||||||
		case AttentionWarning:
 | 
					 | 
				
			||||||
			octiconType = "alert"
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		_, _ = w.WriteString(string(svg.RenderHTML("octicon-" + octiconType)))
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		_, _ = w.WriteString("</span>\n")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return ast.WalkContinue, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 | 
					func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 | 
				
			||||||
	n := node.(*ast.Document)
 | 
						n := node.(*ast.Document)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,7 @@ import (
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/markup"
 | 
						"code.gitea.io/gitea/modules/markup"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/markup/common"
 | 
						"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/markup/markdown/math"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	giteautil "code.gitea.io/gitea/modules/util"
 | 
						giteautil "code.gitea.io/gitea/modules/util"
 | 
				
			||||||
| 
						 | 
					@ -124,6 +125,8 @@ func SpecializedMarkdown() goldmark.Markdown {
 | 
				
			||||||
				parser.WithAttribute(),
 | 
									parser.WithAttribute(),
 | 
				
			||||||
				parser.WithAutoHeadingID(),
 | 
									parser.WithAutoHeadingID(),
 | 
				
			||||||
				parser.WithASTTransformers(
 | 
									parser.WithASTTransformers(
 | 
				
			||||||
 | 
										util.Prioritized(&callout.GitHubLegacyCalloutTransformer{}, 8000),
 | 
				
			||||||
 | 
										util.Prioritized(&callout.GitHubCalloutTransformer{}, 9000),
 | 
				
			||||||
					util.Prioritized(&ASTTransformer{}, 10000),
 | 
										util.Prioritized(&ASTTransformer{}, 10000),
 | 
				
			||||||
				),
 | 
									),
 | 
				
			||||||
			),
 | 
								),
 | 
				
			||||||
| 
						 | 
					@ -135,6 +138,7 @@ func SpecializedMarkdown() goldmark.Markdown {
 | 
				
			||||||
		// Override the original Tasklist renderer!
 | 
							// Override the original Tasklist renderer!
 | 
				
			||||||
		specMarkdown.Renderer().AddOptions(
 | 
							specMarkdown.Renderer().AddOptions(
 | 
				
			||||||
			renderer.WithNodeRenderers(
 | 
								renderer.WithNodeRenderers(
 | 
				
			||||||
 | 
									util.Prioritized(callout.NewGitHubCalloutHTMLRenderer(), 10),
 | 
				
			||||||
				util.Prioritized(NewHTMLRenderer(), 10),
 | 
									util.Prioritized(NewHTMLRenderer(), 10),
 | 
				
			||||||
			),
 | 
								),
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -64,9 +64,10 @@ func createDefaultPolicy() *bluemonday.Policy {
 | 
				
			||||||
	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
 | 
						policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// For attention
 | 
						// For attention
 | 
				
			||||||
 | 
						policy.AllowAttrs("class").Matching(regexp.MustCompile(`^gt-py-3 attention attention-\w+$`)).OnElements("blockquote")
 | 
				
			||||||
	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong")
 | 
						policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong")
 | 
				
			||||||
	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+$`)).OnElements("span", "strong")
 | 
						policy.AllowAttrs("class").Matching(regexp.MustCompile(`^gt-mr-2 gt-vm attention-\w+$`)).OnElements("span", "strong")
 | 
				
			||||||
	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^svg octicon-\w+$`)).OnElements("svg")
 | 
						policy.AllowAttrs("class").Matching(regexp.MustCompile(`^svg octicon-(\w|-)+$`)).OnElements("svg")
 | 
				
			||||||
	policy.AllowAttrs("viewBox", "width", "height", "aria-hidden").OnElements("svg")
 | 
						policy.AllowAttrs("viewBox", "width", "height", "aria-hidden").OnElements("svg")
 | 
				
			||||||
	policy.AllowAttrs("fill-rule", "d").OnElements("path")
 | 
						policy.AllowAttrs("fill-rule", "d").OnElements("path")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										72
									
								
								tests/integration/markup_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								tests/integration/markup_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,72 @@
 | 
				
			||||||
 | 
					// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package integration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						auth_model "code.gitea.io/gitea/models/auth"
 | 
				
			||||||
 | 
						api "code.gitea.io/gitea/modules/structs"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/tests"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestRenderAlertBlocks(t *testing.T) {
 | 
				
			||||||
 | 
						defer tests.PrepareTestEnv(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						session := loginUser(t, "user1")
 | 
				
			||||||
 | 
						token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteMisc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assertAlertBlock := func(t *testing.T, input, alertType, alertIcon string) {
 | 
				
			||||||
 | 
							t.Helper()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							blockquoteAttr := fmt.Sprintf(`<blockquote class="gt-py-3 attention attention-%s"`, strings.ToLower(alertType))
 | 
				
			||||||
 | 
							classAttr := fmt.Sprintf(`class="attention-%s"`, strings.ToLower(alertType))
 | 
				
			||||||
 | 
							iconAttr := fmt.Sprintf(`class="svg octicon-%s"`, alertIcon)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req := NewRequestWithJSON(t, "POST", "/api/v1/markdown", &api.MarkdownOption{
 | 
				
			||||||
 | 
								Text: input,
 | 
				
			||||||
 | 
								Mode: "markdown",
 | 
				
			||||||
 | 
							}).AddTokenAuth(token)
 | 
				
			||||||
 | 
							resp := MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
							body := resp.Body.String()
 | 
				
			||||||
 | 
							assert.Contains(t, body, blockquoteAttr)
 | 
				
			||||||
 | 
							assert.Contains(t, body, classAttr)
 | 
				
			||||||
 | 
							assert.Contains(t, body, iconAttr)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("legacy style", func(t *testing.T) {
 | 
				
			||||||
 | 
							for alertType, alertIcon := range map[string]string{"Note": "info", "Warning": "alert"} {
 | 
				
			||||||
 | 
								t.Run(alertType, func(t *testing.T) {
 | 
				
			||||||
 | 
									input := fmt.Sprintf(`> **%s**
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					> This is a %s.`, alertType, alertType)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									assertAlertBlock(t, input, alertType, alertIcon)
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("modern style", func(t *testing.T) {
 | 
				
			||||||
 | 
							for alertType, alertIcon := range map[string]string{
 | 
				
			||||||
 | 
								"NOTE":      "info",
 | 
				
			||||||
 | 
								"TIP":       "light-bulb",
 | 
				
			||||||
 | 
								"IMPORTANT": "report",
 | 
				
			||||||
 | 
								"WARNING":   "alert",
 | 
				
			||||||
 | 
								"CAUTION":   "stop",
 | 
				
			||||||
 | 
							} {
 | 
				
			||||||
 | 
								t.Run(alertType, func(t *testing.T) {
 | 
				
			||||||
 | 
									input := fmt.Sprintf(`> [!%s]
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					> This is a %s.`, alertType, alertType)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									assertAlertBlock(t, input, alertType, alertIcon)
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1268,20 +1268,45 @@ img.ui.avatar,
 | 
				
			||||||
  border-radius: var(--border-radius);
 | 
					  border-radius: var(--border-radius);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.attention-icon {
 | 
					.attention {
 | 
				
			||||||
  vertical-align: text-top;
 | 
					  color: var(--color-text) !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.attention-note {
 | 
					blockquote.attention-note {
 | 
				
			||||||
  font-weight: unset;
 | 
					  border-left-color: var(--color-blue-dark-1);
 | 
				
			||||||
  color: var(--color-info-text);
 | 
					}
 | 
				
			||||||
 | 
					strong.attention-note, span.attention-note {
 | 
				
			||||||
 | 
					  color: var(--color-blue-dark-1);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.attention-warning {
 | 
					blockquote.attention-tip {
 | 
				
			||||||
  font-weight: unset;
 | 
					  border-left-color: var(--color-success-text);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					strong.attention-tip, span.attention-tip {
 | 
				
			||||||
 | 
					  color: var(--color-success-text);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					blockquote.attention-important {
 | 
				
			||||||
 | 
					  border-left-color: var(--color-violet-dark-1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					strong.attention-important, span.attention-important {
 | 
				
			||||||
 | 
					  color: var(--color-violet-dark-1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					blockquote.attention-warning {
 | 
				
			||||||
 | 
					  border-left-color: var(--color-warning-text);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					strong.attention-warning, span.attention-warning {
 | 
				
			||||||
  color: var(--color-warning-text);
 | 
					  color: var(--color-warning-text);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					blockquote.attention-caution {
 | 
				
			||||||
 | 
					  border-left-color: var(--color-red-dark-1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					strong.attention-caution, span.attention-caution {
 | 
				
			||||||
 | 
					  color: var(--color-red-dark-1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.center:not(.popup) {
 | 
					.center:not(.popup) {
 | 
				
			||||||
  text-align: center;
 | 
					  text-align: center;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue