gitea/modules/markup/markdown/transform_blockquote.go

155 lines
4.9 KiB
Go
Raw Normal View History

// 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
}
2024-10-31 20:05:54 +08:00
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
}