Skip to content

Commit 5db4c8d

Browse files
authored
Refactor backend SVG package and add tests (#26335)
Introduce a well-tested `svg.Normalize` function. Make `RenderHTML` faster and more stable.
1 parent 12c249c commit 5db4c8d

File tree

4 files changed

+111
-38
lines changed

4 files changed

+111
-38
lines changed

modules/html/html.go

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,20 @@ package html
66
// ParseSizeAndClass get size and class from string with default values
77
// If present, "others" expects the new size first and then the classes to use
88
func ParseSizeAndClass(defaultSize int, defaultClass string, others ...any) (int, string) {
9-
if len(others) == 0 {
10-
return defaultSize, defaultClass
11-
}
12-
139
size := defaultSize
14-
_size, ok := others[0].(int)
15-
if ok && _size != 0 {
16-
size = _size
17-
}
18-
19-
if len(others) == 1 {
20-
return size, defaultClass
10+
if len(others) >= 1 {
11+
if v, ok := others[0].(int); ok && v != 0 {
12+
size = v
13+
}
2114
}
22-
2315
class := defaultClass
24-
if _class, ok := others[1].(string); ok && _class != "" {
25-
if defaultClass == "" {
26-
class = _class
27-
} else {
28-
class = defaultClass + " " + _class
16+
if len(others) >= 2 {
17+
if v, ok := others[1].(string); ok && v != "" {
18+
if class != "" {
19+
class += " "
20+
}
21+
class += v
2922
}
3023
}
31-
3224
return size, class
3325
}

modules/svg/processor.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package svg
5+
6+
import (
7+
"bytes"
8+
"fmt"
9+
"regexp"
10+
"sync"
11+
)
12+
13+
type normalizeVarsStruct struct {
14+
reXMLDoc,
15+
reComment,
16+
reAttrXMLNs,
17+
reAttrSize,
18+
reAttrClassPrefix *regexp.Regexp
19+
}
20+
21+
var (
22+
normalizeVars *normalizeVarsStruct
23+
normalizeVarsOnce sync.Once
24+
)
25+
26+
// Normalize normalizes the SVG content: set default width/height, remove unnecessary tags/attributes
27+
// It's designed to work with valid SVG content. For invalid SVG content, the returned content is not guaranteed.
28+
func Normalize(data []byte, size int) []byte {
29+
normalizeVarsOnce.Do(func() {
30+
normalizeVars = &normalizeVarsStruct{
31+
reXMLDoc: regexp.MustCompile(`(?s)<\?xml.*?>`),
32+
reComment: regexp.MustCompile(`(?s)<!--.*?-->`),
33+
34+
reAttrXMLNs: regexp.MustCompile(`(?s)\s+xmlns\s*=\s*"[^"]*"`),
35+
reAttrSize: regexp.MustCompile(`(?s)\s+(width|height)\s*=\s*"[^"]+"`),
36+
reAttrClassPrefix: regexp.MustCompile(`(?s)\s+class\s*=\s*"`),
37+
}
38+
})
39+
data = normalizeVars.reXMLDoc.ReplaceAll(data, nil)
40+
data = normalizeVars.reComment.ReplaceAll(data, nil)
41+
42+
data = bytes.TrimSpace(data)
43+
svgTag, svgRemaining, ok := bytes.Cut(data, []byte(">"))
44+
if !ok || !bytes.HasPrefix(svgTag, []byte(`<svg`)) {
45+
return data
46+
}
47+
normalized := bytes.Clone(svgTag)
48+
normalized = normalizeVars.reAttrXMLNs.ReplaceAll(normalized, nil)
49+
normalized = normalizeVars.reAttrSize.ReplaceAll(normalized, nil)
50+
normalized = normalizeVars.reAttrClassPrefix.ReplaceAll(normalized, []byte(` class="`))
51+
normalized = bytes.TrimSpace(normalized)
52+
normalized = fmt.Appendf(normalized, ` width="%d" height="%d"`, size, size)
53+
if !bytes.Contains(normalized, []byte(` class="`)) {
54+
normalized = append(normalized, ` class="svg"`...)
55+
}
56+
normalized = append(normalized, '>')
57+
normalized = append(normalized, svgRemaining...)
58+
return normalized
59+
}

modules/svg/processor_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package svg
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestNormalize(t *testing.T) {
13+
res := Normalize([]byte("foo"), 1)
14+
assert.Equal(t, "foo", string(res))
15+
16+
res = Normalize([]byte(`<?xml version="1.0"?>
17+
<!--
18+
comment
19+
-->
20+
<svg xmlns = "...">content</svg>`), 1)
21+
assert.Equal(t, `<svg width="1" height="1" class="svg">content</svg>`, string(res))
22+
23+
res = Normalize([]byte(`<svg
24+
width="100"
25+
class="svg-icon"
26+
>content</svg>`), 16)
27+
28+
assert.Equal(t, `<svg class="svg-icon" width="16" height="16">content</svg>`, string(res))
29+
}

modules/svg/svg.go

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,55 +7,48 @@ import (
77
"fmt"
88
"html/template"
99
"path"
10-
"regexp"
1110
"strings"
1211

13-
"code.gitea.io/gitea/modules/html"
12+
gitea_html "code.gitea.io/gitea/modules/html"
1413
"code.gitea.io/gitea/modules/log"
1514
"code.gitea.io/gitea/modules/public"
1615
)
1716

18-
var (
19-
// SVGs contains discovered SVGs
20-
SVGs = map[string]string{}
21-
22-
widthRe = regexp.MustCompile(`width="[0-9]+?"`)
23-
heightRe = regexp.MustCompile(`height="[0-9]+?"`)
24-
)
17+
var svgIcons map[string]string
2518

2619
const defaultSize = 16
2720

28-
// Init discovers SVGs and populates the `SVGs` variable
21+
// Init discovers SVG icons and populates the `svgIcons` variable
2922
func Init() error {
30-
files, err := public.AssetFS().ListFiles("assets/img/svg")
23+
const svgAssetsPath = "assets/img/svg"
24+
files, err := public.AssetFS().ListFiles(svgAssetsPath)
3125
if err != nil {
3226
return err
3327
}
3428

35-
// Remove `xmlns` because inline SVG does not need it
36-
reXmlns := regexp.MustCompile(`(<svg\b[^>]*?)\s+xmlns="[^"]*"`)
29+
svgIcons = make(map[string]string, len(files))
3730
for _, file := range files {
3831
if path.Ext(file) != ".svg" {
3932
continue
4033
}
41-
bs, err := public.AssetFS().ReadFile("assets/img/svg", file)
34+
bs, err := public.AssetFS().ReadFile(svgAssetsPath, file)
4235
if err != nil {
4336
log.Error("Failed to read SVG file %s: %v", file, err)
4437
} else {
45-
SVGs[file[:len(file)-4]] = reXmlns.ReplaceAllString(string(bs), "$1")
38+
svgIcons[file[:len(file)-4]] = string(Normalize(bs, defaultSize))
4639
}
4740
}
4841
return nil
4942
}
5043

5144
// RenderHTML renders icons - arguments icon name (string), size (int), class (string)
5245
func RenderHTML(icon string, others ...any) template.HTML {
53-
size, class := html.ParseSizeAndClass(defaultSize, "", others...)
54-
55-
if svgStr, ok := SVGs[icon]; ok {
46+
size, class := gitea_html.ParseSizeAndClass(defaultSize, "", others...)
47+
if svgStr, ok := svgIcons[icon]; ok {
48+
// the code is somewhat hacky, but it just works, because the SVG contents are all normalized
5649
if size != defaultSize {
57-
svgStr = widthRe.ReplaceAllString(svgStr, fmt.Sprintf(`width="%d"`, size))
58-
svgStr = heightRe.ReplaceAllString(svgStr, fmt.Sprintf(`height="%d"`, size))
50+
svgStr = strings.Replace(svgStr, fmt.Sprintf(`width="%d"`, defaultSize), fmt.Sprintf(`width="%d"`, size), 1)
51+
svgStr = strings.Replace(svgStr, fmt.Sprintf(`height="%d"`, defaultSize), fmt.Sprintf(`height="%d"`, size), 1)
5952
}
6053
if class != "" {
6154
svgStr = strings.Replace(svgStr, `class="`, fmt.Sprintf(`class="%s `, class), 1)

0 commit comments

Comments
 (0)