loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Improve template helper functions: string/slice (#24266)

Follow #23328

The improvements:

1. The `contains` functions are covered by tests
2. The inconsistent behavior of `containGeneric` is replaced by
`StringUtils.Contains` and `SliceUtils.Contains`
3. In the future we can move more help functions into XxxUtils to
simplify the `helper.go` and reduce unnecessary global functions.

FAQ:

1. Why it's called `StringUtils.Contains` but not `strings.Contains`
like Golang?

Because our `StringUtils` is not Golang's `strings` package. There will
be our own string functions.

---------

Co-authored-by: silverwind <me@silverwind.io>

authored by

wxiaoguang
silverwind
and committed by
GitHub
88201914 c0d10560

+105 -40
+5 -31
modules/templates/helper.go
··· 15 15 "mime" 16 16 "net/url" 17 17 "path/filepath" 18 - "reflect" 19 18 "regexp" 20 19 "strings" 21 20 "time" ··· 68 67 "PathEscape": url.PathEscape, 69 68 "PathEscapeSegments": util.PathEscapeSegments, 70 69 70 + // utils 71 + "StringUtils": NewStringUtils, 72 + "SliceUtils": NewSliceUtils, 73 + 71 74 // ----------------------------------------------------------------- 72 75 // string / json 76 + // TODO: move string helper functions to StringUtils 73 77 "Join": strings.Join, 74 78 "DotEscape": DotEscape, 75 - "HasPrefix": strings.HasPrefix, 76 79 "EllipsisString": base.EllipsisString, 77 80 "DumpVar": dumpVar, 78 81 ··· 142 145 }, 143 146 "LoadTimes": func(startTime time.Time) string { 144 147 return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms" 145 - }, 146 - 147 - // ----------------------------------------------------------------- 148 - // slice 149 - "containGeneric": func(arr, v interface{}) bool { 150 - arrV := reflect.ValueOf(arr) 151 - if arrV.Kind() == reflect.String && reflect.ValueOf(v).Kind() == reflect.String { 152 - return strings.Contains(arr.(string), v.(string)) 153 - } 154 - if arrV.Kind() == reflect.Slice { 155 - for i := 0; i < arrV.Len(); i++ { 156 - iV := arrV.Index(i) 157 - if !iV.CanInterface() { 158 - continue 159 - } 160 - if iV.Interface() == v { 161 - return true 162 - } 163 - } 164 - } 165 - return false 166 - }, 167 - "contain": func(s []int64, id int64) bool { 168 - for i := 0; i < len(s); i++ { 169 - if s[i] == id { 170 - return true 171 - } 172 - } 173 - return false 174 148 }, 175 149 176 150 // -----------------------------------------------------------------
modules/templates/util.go modules/templates/util_dict.go
+35
modules/templates/util_slice.go
··· 1 + // Copyright 2023 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package templates 5 + 6 + import ( 7 + "fmt" 8 + "reflect" 9 + ) 10 + 11 + type SliceUtils struct{} 12 + 13 + func NewSliceUtils() *SliceUtils { 14 + return &SliceUtils{} 15 + } 16 + 17 + func (su *SliceUtils) Contains(s, v any) bool { 18 + if s == nil { 19 + return false 20 + } 21 + sv := reflect.ValueOf(s) 22 + if sv.Kind() != reflect.Slice && sv.Kind() != reflect.Array { 23 + panic(fmt.Sprintf("invalid type, expected slice or array, but got: %T", s)) 24 + } 25 + for i := 0; i < sv.Len(); i++ { 26 + it := sv.Index(i) 27 + if !it.CanInterface() { 28 + continue 29 + } 30 + if it.Interface() == v { 31 + return true 32 + } 33 + } 34 + return false 35 + }
+20
modules/templates/util_string.go
··· 1 + // Copyright 2023 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package templates 5 + 6 + import "strings" 7 + 8 + type StringUtils struct{} 9 + 10 + func NewStringUtils() *StringUtils { 11 + return &StringUtils{} 12 + } 13 + 14 + func (su *StringUtils) HasPrefix(s, prefix string) bool { 15 + return strings.HasPrefix(s, prefix) 16 + } 17 + 18 + func (su *StringUtils) Contains(s, substr string) bool { 19 + return strings.Contains(s, substr) 20 + }
+36
modules/templates/util_test.go
··· 4 4 package templates 5 5 6 6 import ( 7 + "html/template" 8 + "io" 9 + "strings" 7 10 "testing" 8 11 9 12 "github.com/stretchr/testify/assert" ··· 41 44 assert.Error(t, err) 42 45 } 43 46 } 47 + 48 + func TestUtils(t *testing.T) { 49 + execTmpl := func(code string, data any) string { 50 + tmpl := template.New("test") 51 + tmpl.Funcs(template.FuncMap{"SliceUtils": NewSliceUtils, "StringUtils": NewStringUtils}) 52 + template.Must(tmpl.Parse(code)) 53 + w := &strings.Builder{} 54 + assert.NoError(t, tmpl.Execute(w, data)) 55 + return w.String() 56 + } 57 + 58 + actual := execTmpl("{{SliceUtils.Contains .Slice .Value}}", map[string]any{"Slice": []string{"a", "b"}, "Value": "a"}) 59 + assert.Equal(t, "true", actual) 60 + 61 + actual = execTmpl("{{SliceUtils.Contains .Slice .Value}}", map[string]any{"Slice": []string{"a", "b"}, "Value": "x"}) 62 + assert.Equal(t, "false", actual) 63 + 64 + actual = execTmpl("{{SliceUtils.Contains .Slice .Value}}", map[string]any{"Slice": []int64{1, 2}, "Value": int64(2)}) 65 + assert.Equal(t, "true", actual) 66 + 67 + actual = execTmpl("{{StringUtils.Contains .String .Value}}", map[string]any{"String": "abc", "Value": "b"}) 68 + assert.Equal(t, "true", actual) 69 + 70 + actual = execTmpl("{{StringUtils.Contains .String .Value}}", map[string]any{"String": "abc", "Value": "x"}) 71 + assert.Equal(t, "false", actual) 72 + 73 + tmpl := template.New("test") 74 + tmpl.Funcs(template.FuncMap{"SliceUtils": NewSliceUtils, "StringUtils": NewStringUtils}) 75 + template.Must(tmpl.Parse("{{SliceUtils.Contains .Slice .Value}}")) 76 + // error is like this: `template: test:1:12: executing "test" at <SliceUtils.Contains>: error calling Contains: ...` 77 + err := tmpl.Execute(io.Discard, map[string]any{"Slice": struct{}{}}) 78 + assert.ErrorContains(t, err, "invalid type, expected slice or array") 79 + }
+1 -1
templates/repo/graph/commits.tmpl
··· 35 35 {{range $commit.Refs}} 36 36 {{$refGroup := .RefGroup}} 37 37 {{if eq $refGroup "pull"}} 38 - {{if or (not $.HidePRRefs) (containGeneric $.SelectedBranches .Name)}} 38 + {{if or (not $.HidePRRefs) (SliceUtils.Contains $.SelectedBranches .Name)}} 39 39 <!-- it's intended to use issues not pulls, if it's a pull you will get redirected --> 40 40 <a class="ui labelled icon button basic tiny gt-mr-2" href="{{$.RepoLink}}/{{if $.Repository.UnitEnabled $.Context $.UnitTypePullRequests}}pulls{{else}}issues{{end}}/{{.ShortName|PathEscape}}"> 41 41 {{svg "octicon-git-pull-request" 16 "gt-mr-2"}}#{{.ShortName}}
+1 -1
templates/repo/header.tmpl
··· 217 217 {{end}} 218 218 219 219 {{if or (.Permission.CanRead $.UnitTypeWiki) (.Permission.CanRead $.UnitTypeExternalWiki)}} 220 - <a class="{{if .PageIsWiki}}active {{end}}item" href="{{.RepoLink}}/wiki" {{if and (.Permission.CanRead $.UnitTypeExternalWiki) (not (HasPrefix ((.Repository.MustGetUnit $.Context $.UnitTypeExternalWiki).ExternalWikiConfig.ExternalWikiURL) (.Repository.Link)))}} target="_blank" rel="noopener noreferrer" {{end}}> 220 + <a class="{{if .PageIsWiki}}active {{end}}item" href="{{.RepoLink}}/wiki" {{if and (.Permission.CanRead $.UnitTypeExternalWiki) (not (StringUtils.HasPrefix ((.Repository.MustGetUnit $.Context $.UnitTypeExternalWiki).ExternalWikiConfig.ExternalWikiURL) (.Repository.Link)))}} target="_blank" rel="noopener noreferrer" {{end}}> 221 221 {{svg "octicon-book"}} {{.locale.Tr "repo.wiki"}} 222 222 </a> 223 223 {{end}}
+1 -1
templates/repo/issue/list.tmpl
··· 227 227 {{end}} 228 228 {{$previousExclusiveScope = $exclusiveScope}} 229 229 <div class="item issue-action" data-action="toggle" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/labels"> 230 - {{if contain $.SelLabelIDs .ID}}{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}{{end}} {{RenderLabel $.Context .}} 230 + {{if SliceUtils.Contains $.SelLabelIDs .ID}}{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}{{end}} {{RenderLabel $.Context .}} 231 231 </div> 232 232 {{end}} 233 233 </div>
+2 -2
templates/repo/issue/milestone_issues.tmpl
··· 65 65 <span class="info">{{.locale.Tr "repo.issues.filter_label_exclude" | Safe}}</span> 66 66 <a class="item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}">{{.locale.Tr "repo.issues.filter_label_no_select"}}</a> 67 67 {{range .Labels}} 68 - <a class="item label-filter-item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.QueryString}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}" data-label-id="{{.ID}}">{{if .IsExcluded}}{{svg "octicon-circle-slash"}}{{else if contain $.SelLabelIDs .ID}}{{svg "octicon-check"}}{{end}} {{RenderLabel $.Context .}}</a> 68 + <a class="item label-filter-item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.QueryString}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}" data-label-id="{{.ID}}">{{if .IsExcluded}}{{svg "octicon-circle-slash"}}{{else if SliceUtils.Contains $.SelLabelIDs .ID}}{{svg "octicon-check"}}{{end}} {{RenderLabel $.Context .}}</a> 69 69 {{end}} 70 70 </div> 71 71 </div> ··· 171 171 <div class="menu"> 172 172 {{range .Labels}} 173 173 <div class="item issue-action" data-action="toggle" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/labels"> 174 - {{if contain $.SelLabelIDs .ID}}{{svg "octicon-check"}}{{end}} {{RenderLabel $.Context .}} 174 + {{if SliceUtils.Contains $.SelLabelIDs .ID}}{{svg "octicon-check"}}{{end}} {{RenderLabel $.Context .}} 175 175 </div> 176 176 {{end}} 177 177 </div>
+2 -2
templates/repo/issue/view_content/attachments.tmpl
··· 8 8 <div class="gt-f1 gt-p-3"> 9 9 <a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}" title='{{$.ctxData.locale.Tr "repo.issues.attachment.open_tab" .Name}}'> 10 10 {{if FilenameIsImage .Name}} 11 - {{if not (containGeneric $.Content .UUID)}} 11 + {{if not (StringUtils.Contains $.Content .UUID)}} 12 12 {{$hasThumbnails = true}} 13 13 {{end}} 14 14 {{svg "octicon-file"}} ··· 29 29 <div class="ui small thumbnails"> 30 30 {{- range .Attachments -}} 31 31 {{if FilenameIsImage .Name}} 32 - {{if not (containGeneric $.Content .UUID)}} 32 + {{if not (StringUtils.Contains $.Content .UUID)}} 33 33 <a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}"> 34 34 <img alt="{{.Name}}" src="{{.DownloadURL}}" title='{{$.ctxData.locale.Tr "repo.issues.attachment.open_tab" .Name}}'> 35 35 </a>
+2 -2
templates/repo/settings/tags.tmpl
··· 92 92 {{if or .AllowlistUserIDs (and $.Owner.IsOrganization .AllowlistTeamIDs)}} 93 93 {{$userIDs := .AllowlistUserIDs}} 94 94 {{range $.Users}} 95 - {{if contain $userIDs .ID}} 95 + {{if SliceUtils.Contains $userIDs .ID}} 96 96 <a class="ui basic label" href="{{.HomeLink}}">{{avatar $.Context . 26}} {{.GetDisplayName}}</a> 97 97 {{end}} 98 98 {{end}} 99 99 {{if $.Owner.IsOrganization}} 100 100 {{$teamIDs := .AllowlistTeamIDs}} 101 101 {{range $.Teams}} 102 - {{if contain $teamIDs .ID}} 102 + {{if SliceUtils.Contains $teamIDs .ID}} 103 103 <a class="ui basic label" href="{{$.Owner.OrganisationLink}}/teams/{{PathEscape .LowerName}}">{{.Name}}</a> 104 104 {{end}} 105 105 {{end}}