// Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package markdown import ( "strings" "code.gitea.io/gitea/modules/svg" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/text" "github.com/yuin/goldmark/util" "golang.org/x/text/cases" "golang.org/x/text/language" ) // 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 { n := node.(*Attention) var octiconName string switch n.AttentionType { case "tip": octiconName = "light-bulb" case "important": octiconName = "report" case "warning": octiconName = "alert" case "caution": octiconName = "stop" default: // including "note" octiconName = "info" } svgHTML := svg.RenderHTML("octicon-"+octiconName, 16, "attention-icon attention-"+n.AttentionType) _, _ = w.WriteString(string(r.renderInternal.ProtectSafeAttrs(svgHTML))) } return ast.WalkContinue, nil } func (g *ASTTransformer) extractBlockquoteAttentionEmphasis(firstParagraph ast.Node, reader text.Reader) (string, []ast.Node) { if firstParagraph.ChildCount() < 1 { return "", nil } node1, ok := firstParagraph.FirstChild().(*ast.Emphasis) if !ok { return "", nil } val1 := string(node1.Text(reader.Source())) //nolint:staticcheck attentionType := strings.ToLower(val1) if g.attentionTypes.Contains(attentionType) { return attentionType, []ast.Node{node1} } return "", nil } func (g *ASTTransformer) extractBlockquoteAttention2(firstParagraph ast.Node, reader text.Reader) (string, []ast.Node) { if firstParagraph.ChildCount() < 2 { return "", nil } node1, ok := firstParagraph.FirstChild().(*ast.Text) if !ok { return "", nil } node2, ok := node1.NextSibling().(*ast.Text) if !ok { return "", nil } val1 := string(node1.Segment.Value(reader.Source())) val2 := string(node2.Segment.Value(reader.Source())) if strings.HasPrefix(val1, `\[!`) && val2 == `\]` { attentionType := strings.ToLower(val1[3:]) if g.attentionTypes.Contains(attentionType) { return attentionType, []ast.Node{node1, node2} } } return "", nil } func (g *ASTTransformer) extractBlockquoteAttention3(firstParagraph ast.Node, reader text.Reader) (string, []ast.Node) { if firstParagraph.ChildCount() < 3 { return "", nil } node1, ok := firstParagraph.FirstChild().(*ast.Text) if !ok { return "", nil } node2, ok := node1.NextSibling().(*ast.Text) if !ok { return "", nil } node3, ok := node2.NextSibling().(*ast.Text) if !ok { return "", nil } val1 := string(node1.Segment.Value(reader.Source())) val2 := string(node2.Segment.Value(reader.Source())) val3 := string(node3.Segment.Value(reader.Source())) if val1 != "[" || val3 != "]" || !strings.HasPrefix(val2, "!") { return "", nil } attentionType := strings.ToLower(val2[1:]) if g.attentionTypes.Contains(attentionType) { return attentionType, []ast.Node{node1, node2, node3} } return "", nil } func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Reader) (ast.WalkStatus, error) { // We only want attention blockquotes when the AST looks like: // > Text("[") Text("!TYPE") Text("]") // > Text("\[!TYPE") TEXT("\]") // > Text("**TYPE**") // grab these nodes and make sure we adhere to the attention blockquote structure firstParagraph := v.FirstChild() g.applyElementDir(firstParagraph) attentionType, processedNodes := g.extractBlockquoteAttentionEmphasis(firstParagraph, reader) if attentionType == "" { attentionType, processedNodes = g.extractBlockquoteAttention2(firstParagraph, reader) } if attentionType == "" { attentionType, processedNodes = g.extractBlockquoteAttention3(firstParagraph, reader) } if attentionType == "" { return ast.WalkContinue, nil } // color the blockquote v.SetAttributeString(g.renderInternal.SafeAttr("class"), []byte(g.renderInternal.SafeValue("attention-header attention-"+attentionType))) // create an emphasis to make it bold attentionParagraph := ast.NewParagraph() g.applyElementDir(attentionParagraph) emphasis := ast.NewEmphasis(2) emphasis.SetAttributeString(g.renderInternal.SafeAttr("class"), []byte(g.renderInternal.SafeValue("attention-"+attentionType))) attentionAstString := ast.NewString([]byte(cases.Title(language.English).String(attentionType))) // replace the ![TYPE] with a dedicated paragraph of icon+Type emphasis.AppendChild(emphasis, attentionAstString) attentionParagraph.AppendChild(attentionParagraph, NewAttention(attentionType)) attentionParagraph.AppendChild(attentionParagraph, emphasis) firstParagraph.Parent().InsertBefore(firstParagraph.Parent(), firstParagraph, attentionParagraph) for _, processed := range processedNodes { firstParagraph.RemoveChild(firstParagraph, processed) } if firstParagraph.ChildCount() == 0 { firstParagraph.Parent().RemoveChild(firstParagraph.Parent(), firstParagraph) } return ast.WalkContinue, nil }