summaryrefslogtreecommitdiffstats
path: root/modules/markup/markdown/callout/github.go
blob: debad42b83040075c772240da6d93e3dcfb8ded0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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:
			if v.ChildCount() == 0 {
				return ast.WalkContinue, nil
			}

			// 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("attention-header attention-"+attentionType))

			// create an emphasis to make it bold
			attentionParagraph := ast.NewParagraph()
			attentionParagraph.SetAttributeString("class", []byte("attention-title"))
			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 a dedicated paragraph of icon+Type
			emphasis.AppendChild(emphasis, attentionText)
			attentionParagraph.AppendChild(attentionParagraph, NewAttention(attentionType))
			attentionParagraph.AppendChild(attentionParagraph, emphasis)
			firstParagraph.Parent().InsertBefore(firstParagraph.Parent(), firstParagraph, attentionParagraph)
			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 {
		n := node.(*Attention)

		var octiconName string
		switch n.AttentionType {
		case "note":
			octiconName = "info"
		case "tip":
			octiconName = "light-bulb"
		case "important":
			octiconName = "report"
		case "warning":
			octiconName = "alert"
		case "caution":
			octiconName = "stop"
		default:
			octiconName = "info"
		}
		_, _ = w.WriteString(string(svg.RenderHTML("octicon-"+octiconName, 16, "attention-icon attention-"+n.AttentionType)))
	}
	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
}