Skip to content

Commit 8f09802

Browse files
committed
fix
1 parent 2f060c5 commit 8f09802

File tree

5 files changed

+142
-65
lines changed

5 files changed

+142
-65
lines changed

modules/markup/markdown/goldmark.go

+24-63
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,21 @@ import (
2727
)
2828

2929
// ASTTransformer is a default transformer of the goldmark tree.
30-
type ASTTransformer struct{}
30+
type ASTTransformer struct {
31+
AttentionTypes container.Set[string]
32+
}
33+
34+
func NewASTTransformer() *ASTTransformer {
35+
return &ASTTransformer{
36+
AttentionTypes: container.SetOf("note", "tip", "important", "warning", "caution"),
37+
}
38+
}
39+
40+
func (g *ASTTransformer) applyElementDir(n ast.Node) {
41+
if markup.DefaultProcessorHelper.ElementDir != "" {
42+
n.SetAttributeString("dir", []byte(markup.DefaultProcessorHelper.ElementDir))
43+
}
44+
}
3145

3246
// Transform transforms the given AST tree.
3347
func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
@@ -45,12 +59,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
4559
tocMode = rc.TOC
4660
}
4761

48-
applyElementDir := func(n ast.Node) {
49-
if markup.DefaultProcessorHelper.ElementDir != "" {
50-
n.SetAttributeString("dir", []byte(markup.DefaultProcessorHelper.ElementDir))
51-
}
52-
}
53-
5462
_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
5563
if !entering {
5664
return ast.WalkContinue, nil
@@ -72,9 +80,9 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
7280
header.ID = util.BytesToReadOnlyString(id.([]byte))
7381
}
7482
tocList = append(tocList, header)
75-
applyElementDir(v)
83+
g.applyElementDir(v)
7684
case *ast.Paragraph:
77-
applyElementDir(v)
85+
g.applyElementDir(v)
7886
case *ast.Image:
7987
// Images need two things:
8088
//
@@ -174,7 +182,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
174182
v.AppendChild(v, newChild)
175183
}
176184
}
177-
applyElementDir(v)
185+
g.applyElementDir(v)
178186
case *ast.Text:
179187
if v.SoftLineBreak() && !v.HardLineBreak() {
180188
if ctx.Metas["mode"] != "document" {
@@ -189,51 +197,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
189197
v.AppendChild(v, NewColorPreview(colorContent))
190198
}
191199
case *ast.Blockquote:
192-
// We only want attention blockquotes when the AST looks like:
193-
// Text: "["
194-
// Text: "!TYPE"
195-
// Text(SoftLineBreak): "]"
196-
197-
// grab these nodes and make sure we adhere to the attention blockquote structure
198-
firstParagraph := v.FirstChild()
199-
if firstParagraph.ChildCount() < 3 {
200-
return ast.WalkContinue, nil
201-
}
202-
firstTextNode, ok := firstParagraph.FirstChild().(*ast.Text)
203-
if !ok || string(firstTextNode.Segment.Value(reader.Source())) != "[" {
204-
return ast.WalkContinue, nil
205-
}
206-
secondTextNode, ok := firstTextNode.NextSibling().(*ast.Text)
207-
if !ok || !attentionTypeRE.MatchString(string(secondTextNode.Segment.Value(reader.Source()))) {
208-
return ast.WalkContinue, nil
209-
}
210-
thirdTextNode, ok := secondTextNode.NextSibling().(*ast.Text)
211-
if !ok || string(thirdTextNode.Segment.Value(reader.Source())) != "]" {
212-
return ast.WalkContinue, nil
213-
}
214-
215-
// grab attention type from markdown source
216-
attentionType := strings.ToLower(strings.TrimPrefix(string(secondTextNode.Segment.Value(reader.Source())), "!"))
217-
218-
// color the blockquote
219-
v.SetAttributeString("class", []byte("attention-header attention-"+attentionType))
220-
221-
// create an emphasis to make it bold
222-
attentionParagraph := ast.NewParagraph()
223-
emphasis := ast.NewEmphasis(2)
224-
emphasis.SetAttributeString("class", []byte("attention-"+attentionType))
225-
226-
// capitalize first letter
227-
attentionText := ast.NewString([]byte(strings.ToUpper(string(attentionType[0])) + attentionType[1:]))
228-
229-
// replace the ![TYPE] with a dedicated paragraph of icon+Type
230-
emphasis.AppendChild(emphasis, attentionText)
231-
attentionParagraph.AppendChild(attentionParagraph, NewAttention(attentionType))
232-
attentionParagraph.AppendChild(attentionParagraph, emphasis)
233-
firstParagraph.Parent().InsertBefore(firstParagraph.Parent(), firstParagraph, attentionParagraph)
234-
firstParagraph.RemoveChild(firstParagraph, firstTextNode)
235-
firstParagraph.RemoveChild(firstParagraph, secondTextNode)
236-
firstParagraph.RemoveChild(firstParagraph, thirdTextNode)
200+
return g.transformBlockquote(v, reader)
237201
}
238202
return ast.WalkContinue, nil
239203
})
@@ -268,7 +232,7 @@ func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte {
268232
return p.GenerateWithDefault(value, dft)
269233
}
270234

271-
// Generate generates a new element id.
235+
// GenerateWithDefault generates a new element id.
272236
func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
273237
result := common.CleanValue(value)
274238
if len(result) == 0 {
@@ -303,7 +267,8 @@ func newPrefixedIDs() *prefixedIDs {
303267
// in the gitea form.
304268
func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
305269
r := &HTMLRenderer{
306-
Config: html.NewConfig(),
270+
Config: html.NewConfig(),
271+
reValidName: regexp.MustCompile("^[a-z ]+$"),
307272
}
308273
for _, opt := range opts {
309274
opt.SetHTMLOption(&r.Config)
@@ -315,6 +280,7 @@ func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
315280
// renders gitea specific features.
316281
type HTMLRenderer struct {
317282
html.Config
283+
reValidName *regexp.Regexp
318284
}
319285

320286
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
@@ -442,11 +408,6 @@ func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.N
442408
return ast.WalkContinue, nil
443409
}
444410

445-
var (
446-
validNameRE = regexp.MustCompile("^[a-z ]+$")
447-
attentionTypeRE = regexp.MustCompile("^!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)$")
448-
)
449-
450411
func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
451412
if !entering {
452413
return ast.WalkContinue, nil
@@ -461,7 +422,7 @@ func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node
461422
return ast.WalkContinue, nil
462423
}
463424

464-
if !validNameRE.MatchString(name) {
425+
if !r.reValidName.MatchString(name) {
465426
// skip this
466427
return ast.WalkContinue, nil
467428
}

modules/markup/markdown/markdown.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ func SpecializedMarkdown() goldmark.Markdown {
126126
parser.WithAttribute(),
127127
parser.WithAutoHeadingID(),
128128
parser.WithASTTransformers(
129-
util.Prioritized(&ASTTransformer{}, 10000),
129+
util.Prioritized(NewASTTransformer(), 10000),
130130
),
131131
),
132132
goldmark.WithRendererOptions(

modules/markup/markdown/markdown_test.go

+37
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package markdown_test
55

66
import (
77
"context"
8+
"fmt"
89
"html/template"
910
"os"
1011
"strings"
@@ -16,9 +17,12 @@ import (
1617
"code.gitea.io/gitea/modules/markup"
1718
"code.gitea.io/gitea/modules/markup/markdown"
1819
"code.gitea.io/gitea/modules/setting"
20+
"code.gitea.io/gitea/modules/svg"
1921
"code.gitea.io/gitea/modules/util"
2022

2123
"github.com/stretchr/testify/assert"
24+
"golang.org/x/text/cases"
25+
"golang.org/x/text/language"
2226
)
2327

2428
const (
@@ -957,3 +961,36 @@ space</p>
957961
assert.Equal(t, template.HTML(c.Expected), result, "Unexpected result in testcase %v", i)
958962
}
959963
}
964+
965+
func TestAttention(t *testing.T) {
966+
defer svg.MockIcon("octicon-info")()
967+
defer svg.MockIcon("octicon-light-bulb")()
968+
defer svg.MockIcon("octicon-report")()
969+
defer svg.MockIcon("octicon-alert")()
970+
defer svg.MockIcon("octicon-stop")()
971+
972+
renderAttention := func(attention, icon string) string {
973+
tmpl := `<blockquote class="attention-header attention-{attention}"><p><svg class="attention-icon attention-{attention} svg {icon}" width="16" height="16"></svg><strong class="attention-{attention}">{Attention}</strong></p>`
974+
tmpl = strings.ReplaceAll(tmpl, "{attention}", attention)
975+
tmpl = strings.ReplaceAll(tmpl, "{icon}", icon)
976+
tmpl = strings.ReplaceAll(tmpl, "{Attention}", cases.Title(language.English).String(attention))
977+
return tmpl
978+
}
979+
980+
test := func(input, expected string) {
981+
result, err := markdown.RenderString(&markup.RenderContext{Ctx: context.Background()}, input)
982+
assert.NoError(t, err)
983+
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(result)))
984+
}
985+
986+
test(`
987+
> [!NOTE]
988+
> text
989+
`, renderAttention("note", "octicon-info")+"\n<p>text</p>\n</blockquote>")
990+
991+
test(fmt.Sprintf(`> [!%s]`, "note"), renderAttention("note", "octicon-info")+"\n</blockquote>")
992+
test(fmt.Sprintf(`> [!%s]`, "tip"), renderAttention("tip", "octicon-light-bulb")+"\n</blockquote>")
993+
test(fmt.Sprintf(`> [!%s]`, "important"), renderAttention("important", "octicon-report")+"\n</blockquote>")
994+
test(fmt.Sprintf(`> [!%s]`, "warning"), renderAttention("warning", "octicon-alert")+"\n</blockquote>")
995+
test(fmt.Sprintf(`> [!%s]`, "caution"), renderAttention("caution", "octicon-stop")+"\n</blockquote>")
996+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package markdown
5+
6+
import (
7+
"strings"
8+
9+
"github.com/yuin/goldmark/ast"
10+
"github.com/yuin/goldmark/text"
11+
"golang.org/x/text/cases"
12+
"golang.org/x/text/language"
13+
)
14+
15+
func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Reader) (ast.WalkStatus, error) {
16+
// We only want attention blockquotes when the AST looks like:
17+
// > Text("[") Text("!TYPE") Text("]")
18+
19+
// grab these nodes and make sure we adhere to the attention blockquote structure
20+
firstParagraph := v.FirstChild()
21+
g.applyElementDir(firstParagraph)
22+
if firstParagraph.ChildCount() < 3 {
23+
return ast.WalkContinue, nil
24+
}
25+
node1, ok1 := firstParagraph.FirstChild().(*ast.Text)
26+
node2, ok2 := node1.NextSibling().(*ast.Text)
27+
node3, ok3 := node2.NextSibling().(*ast.Text)
28+
if !ok1 || !ok2 || !ok3 {
29+
return ast.WalkContinue, nil
30+
}
31+
val1 := string(node1.Segment.Value(reader.Source()))
32+
val2 := string(node2.Segment.Value(reader.Source()))
33+
val3 := string(node3.Segment.Value(reader.Source()))
34+
if val1 != "[" || val3 != "]" || !strings.HasPrefix(val2, "!") {
35+
return ast.WalkContinue, nil
36+
}
37+
38+
// grab attention type from markdown source
39+
attentionType := strings.ToLower(val2[1:])
40+
if !g.AttentionTypes.Contains(attentionType) {
41+
return ast.WalkContinue, nil
42+
}
43+
44+
// color the blockquote
45+
v.SetAttributeString("class", []byte("attention-header attention-"+attentionType))
46+
47+
// create an emphasis to make it bold
48+
attentionParagraph := ast.NewParagraph()
49+
g.applyElementDir(attentionParagraph)
50+
emphasis := ast.NewEmphasis(2)
51+
emphasis.SetAttributeString("class", []byte("attention-"+attentionType))
52+
53+
attentionAstString := ast.NewString([]byte(cases.Title(language.English).String(attentionType)))
54+
55+
// replace the ![TYPE] with a dedicated paragraph of icon+Type
56+
emphasis.AppendChild(emphasis, attentionAstString)
57+
attentionParagraph.AppendChild(attentionParagraph, NewAttention(attentionType))
58+
attentionParagraph.AppendChild(attentionParagraph, emphasis)
59+
firstParagraph.Parent().InsertBefore(firstParagraph.Parent(), firstParagraph, attentionParagraph)
60+
firstParagraph.RemoveChild(firstParagraph, node1)
61+
firstParagraph.RemoveChild(firstParagraph, node2)
62+
firstParagraph.RemoveChild(firstParagraph, node3)
63+
if firstParagraph.ChildCount() == 0 {
64+
firstParagraph.Parent().RemoveChild(firstParagraph.Parent(), firstParagraph)
65+
}
66+
return ast.WalkContinue, nil
67+
}

modules/svg/svg.go

+13-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ func Init() error {
4141
return nil
4242
}
4343

44+
func MockIcon(icon string) func() {
45+
if svgIcons == nil {
46+
svgIcons = make(map[string]string)
47+
}
48+
orig := svgIcons[icon]
49+
svgIcons[icon] = fmt.Sprintf(`<svg class="svg %s" width="%d" height="%d"></svg>`, icon, defaultSize, defaultSize)
50+
return func() {
51+
svgIcons[icon] = orig
52+
}
53+
}
54+
4455
// RenderHTML renders icons - arguments icon name (string), size (int), class (string)
4556
func RenderHTML(icon string, others ...any) template.HTML {
4657
size, class := gitea_html.ParseSizeAndClass(defaultSize, "", others...)
@@ -55,5 +66,6 @@ func RenderHTML(icon string, others ...any) template.HTML {
5566
}
5667
return template.HTML(svgStr)
5768
}
58-
return ""
69+
// during test (or something wrong happens), there is no SVG loaded, so use a dummy span to tell that the icon is missing
70+
return template.HTML(fmt.Sprintf("<span>%s(%d/%s)</span>", template.HTMLEscapeString(icon), size, template.HTMLEscapeString(class)))
5971
}

0 commit comments

Comments
 (0)