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)
 | 
			
		||||
	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/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"
 | 
			
		||||
| 
						 | 
				
			
			@ -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) {
 | 
			
		||||
		if !entering {
 | 
			
		||||
			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))) {
 | 
			
		||||
				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
 | 
			
		||||
	})
 | 
			
		||||
| 
						 | 
				
			
			@ -299,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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -336,28 +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(`<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) {
 | 
			
		||||
	n := node.(*ast.Document)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,8 @@ func SpecializedMarkdown() goldmark.Markdown {
 | 
			
		|||
				parser.WithAttribute(),
 | 
			
		||||
				parser.WithAutoHeadingID(),
 | 
			
		||||
				parser.WithASTTransformers(
 | 
			
		||||
					util.Prioritized(&callout.GitHubLegacyCalloutTransformer{}, 8000),
 | 
			
		||||
					util.Prioritized(&callout.GitHubCalloutTransformer{}, 9000),
 | 
			
		||||
					util.Prioritized(&ASTTransformer{}, 10000),
 | 
			
		||||
				),
 | 
			
		||||
			),
 | 
			
		||||
| 
						 | 
				
			
			@ -135,6 +138,7 @@ func SpecializedMarkdown() goldmark.Markdown {
 | 
			
		|||
		// Override the original Tasklist renderer!
 | 
			
		||||
		specMarkdown.Renderer().AddOptions(
 | 
			
		||||
			renderer.WithNodeRenderers(
 | 
			
		||||
				util.Prioritized(callout.NewGitHubCalloutHTMLRenderer(), 10),
 | 
			
		||||
				util.Prioritized(NewHTMLRenderer(), 10),
 | 
			
		||||
			),
 | 
			
		||||
		)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -64,9 +64,10 @@ func createDefaultPolicy() *bluemonday.Policy {
 | 
			
		|||
	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
 | 
			
		||||
 | 
			
		||||
	// 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-icon attention-\w+$`)).OnElements("span", "strong")
 | 
			
		||||
	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^svg octicon-\w+$`)).OnElements("svg")
 | 
			
		||||
	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("viewBox", "width", "height", "aria-hidden").OnElements("svg")
 | 
			
		||||
	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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.attention-icon {
 | 
			
		||||
  vertical-align: text-top;
 | 
			
		||||
.attention {
 | 
			
		||||
  color: var(--color-text) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.attention-note {
 | 
			
		||||
  font-weight: unset;
 | 
			
		||||
  color: var(--color-info-text);
 | 
			
		||||
blockquote.attention-note {
 | 
			
		||||
  border-left-color: var(--color-blue-dark-1);
 | 
			
		||||
}
 | 
			
		||||
strong.attention-note, span.attention-note {
 | 
			
		||||
  color: var(--color-blue-dark-1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.attention-warning {
 | 
			
		||||
  font-weight: unset;
 | 
			
		||||
blockquote.attention-tip {
 | 
			
		||||
  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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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) {
 | 
			
		||||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue