"mime"
"net/url"
"path/filepath"
- "reflect"
"regexp"
"strings"
"time"
"PathEscape": url.PathEscape,
"PathEscapeSegments": util.PathEscapeSegments,
+ // utils
+ "StringUtils": NewStringUtils,
+ "SliceUtils": NewSliceUtils,
+
// -----------------------------------------------------------------
// string / json
+ // TODO: move string helper functions to StringUtils
"Join": strings.Join,
"DotEscape": DotEscape,
- "HasPrefix": strings.HasPrefix,
"EllipsisString": base.EllipsisString,
"DumpVar": dumpVar,
return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
},
- // -----------------------------------------------------------------
- // slice
- "containGeneric": func(arr, v interface{}) bool {
- arrV := reflect.ValueOf(arr)
- if arrV.Kind() == reflect.String && reflect.ValueOf(v).Kind() == reflect.String {
- return strings.Contains(arr.(string), v.(string))
- }
- if arrV.Kind() == reflect.Slice {
- for i := 0; i < arrV.Len(); i++ {
- iV := arrV.Index(i)
- if !iV.CanInterface() {
- continue
- }
- if iV.Interface() == v {
- return true
- }
- }
- }
- return false
- },
- "contain": func(s []int64, id int64) bool {
- for i := 0; i < len(s); i++ {
- if s[i] == id {
- return true
- }
- }
- return false
- },
-
// -----------------------------------------------------------------
// setting
"AppName": func() string {
+++ /dev/null
-// Copyright 2023 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package templates
-
-import (
- "fmt"
- "html"
- "html/template"
- "reflect"
-
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/setting"
-)
-
-func dictMerge(base map[string]any, arg any) bool {
- if arg == nil {
- return true
- }
- rv := reflect.ValueOf(arg)
- if rv.Kind() == reflect.Map {
- for _, k := range rv.MapKeys() {
- base[k.String()] = rv.MapIndex(k).Interface()
- }
- return true
- }
- return false
-}
-
-// dict is a helper function for creating a map[string]any from a list of key-value pairs.
-// If the key is dot ".", the value is merged into the base map, just like Golang template's dot syntax: dot means current
-// The dot syntax is highly discouraged because it might cause unclear key conflicts. It's always good to use explicit keys.
-func dict(args ...any) (map[string]any, error) {
- if len(args)%2 != 0 {
- return nil, fmt.Errorf("invalid dict constructor syntax: must have key-value pairs")
- }
- m := make(map[string]any, len(args)/2)
- for i := 0; i < len(args); i += 2 {
- key, ok := args[i].(string)
- if !ok {
- return nil, fmt.Errorf("invalid dict constructor syntax: unable to merge args[%d]", i)
- }
- if key == "." {
- if ok = dictMerge(m, args[i+1]); !ok {
- return nil, fmt.Errorf("invalid dict constructor syntax: dot arg[%d] must be followed by a dict", i)
- }
- } else {
- m[key] = args[i+1]
- }
- }
- return m, nil
-}
-
-func dumpVarMarshalable(v any, dumped map[uintptr]bool) (ret any, ok bool) {
- if v == nil {
- return nil, true
- }
- e := reflect.ValueOf(v)
- for e.Kind() == reflect.Pointer {
- e = e.Elem()
- }
- if e.CanAddr() {
- addr := e.UnsafeAddr()
- if dumped[addr] {
- return "[dumped]", false
- }
- dumped[addr] = true
- defer delete(dumped, addr)
- }
- switch e.Kind() {
- case reflect.Bool, reflect.String,
- reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
- reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
- reflect.Float32, reflect.Float64:
- return e.Interface(), true
- case reflect.Struct:
- m := map[string]any{}
- for i := 0; i < e.NumField(); i++ {
- k := e.Type().Field(i).Name
- if !e.Type().Field(i).IsExported() {
- continue
- }
- v := e.Field(i).Interface()
- m[k], _ = dumpVarMarshalable(v, dumped)
- }
- return m, true
- case reflect.Map:
- m := map[string]any{}
- for _, k := range e.MapKeys() {
- m[k.String()], _ = dumpVarMarshalable(e.MapIndex(k).Interface(), dumped)
- }
- return m, true
- case reflect.Array, reflect.Slice:
- var m []any
- for i := 0; i < e.Len(); i++ {
- v, _ := dumpVarMarshalable(e.Index(i).Interface(), dumped)
- m = append(m, v)
- }
- return m, true
- default:
- return "[" + reflect.TypeOf(v).String() + "]", false
- }
-}
-
-// dumpVar helps to dump a variable in a template, to help debugging and development.
-func dumpVar(v any) template.HTML {
- if setting.IsProd {
- return "<pre>dumpVar: only available in dev mode</pre>"
- }
- m, ok := dumpVarMarshalable(v, map[uintptr]bool{})
- dumpStr := ""
- jsonBytes, err := json.MarshalIndent(m, "", " ")
- if err != nil {
- dumpStr = fmt.Sprintf("dumpVar: unable to marshal %T: %v", v, err)
- } else if ok {
- dumpStr = fmt.Sprintf("dumpVar: %T\n%s", v, string(jsonBytes))
- } else {
- dumpStr = fmt.Sprintf("dumpVar: unmarshalable %T\n%s", v, string(jsonBytes))
- }
- return template.HTML("<pre>" + html.EscapeString(dumpStr) + "</pre>")
-}
--- /dev/null
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package templates
+
+import (
+ "fmt"
+ "html"
+ "html/template"
+ "reflect"
+
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+func dictMerge(base map[string]any, arg any) bool {
+ if arg == nil {
+ return true
+ }
+ rv := reflect.ValueOf(arg)
+ if rv.Kind() == reflect.Map {
+ for _, k := range rv.MapKeys() {
+ base[k.String()] = rv.MapIndex(k).Interface()
+ }
+ return true
+ }
+ return false
+}
+
+// dict is a helper function for creating a map[string]any from a list of key-value pairs.
+// If the key is dot ".", the value is merged into the base map, just like Golang template's dot syntax: dot means current
+// The dot syntax is highly discouraged because it might cause unclear key conflicts. It's always good to use explicit keys.
+func dict(args ...any) (map[string]any, error) {
+ if len(args)%2 != 0 {
+ return nil, fmt.Errorf("invalid dict constructor syntax: must have key-value pairs")
+ }
+ m := make(map[string]any, len(args)/2)
+ for i := 0; i < len(args); i += 2 {
+ key, ok := args[i].(string)
+ if !ok {
+ return nil, fmt.Errorf("invalid dict constructor syntax: unable to merge args[%d]", i)
+ }
+ if key == "." {
+ if ok = dictMerge(m, args[i+1]); !ok {
+ return nil, fmt.Errorf("invalid dict constructor syntax: dot arg[%d] must be followed by a dict", i)
+ }
+ } else {
+ m[key] = args[i+1]
+ }
+ }
+ return m, nil
+}
+
+func dumpVarMarshalable(v any, dumped map[uintptr]bool) (ret any, ok bool) {
+ if v == nil {
+ return nil, true
+ }
+ e := reflect.ValueOf(v)
+ for e.Kind() == reflect.Pointer {
+ e = e.Elem()
+ }
+ if e.CanAddr() {
+ addr := e.UnsafeAddr()
+ if dumped[addr] {
+ return "[dumped]", false
+ }
+ dumped[addr] = true
+ defer delete(dumped, addr)
+ }
+ switch e.Kind() {
+ case reflect.Bool, reflect.String,
+ reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
+ reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
+ reflect.Float32, reflect.Float64:
+ return e.Interface(), true
+ case reflect.Struct:
+ m := map[string]any{}
+ for i := 0; i < e.NumField(); i++ {
+ k := e.Type().Field(i).Name
+ if !e.Type().Field(i).IsExported() {
+ continue
+ }
+ v := e.Field(i).Interface()
+ m[k], _ = dumpVarMarshalable(v, dumped)
+ }
+ return m, true
+ case reflect.Map:
+ m := map[string]any{}
+ for _, k := range e.MapKeys() {
+ m[k.String()], _ = dumpVarMarshalable(e.MapIndex(k).Interface(), dumped)
+ }
+ return m, true
+ case reflect.Array, reflect.Slice:
+ var m []any
+ for i := 0; i < e.Len(); i++ {
+ v, _ := dumpVarMarshalable(e.Index(i).Interface(), dumped)
+ m = append(m, v)
+ }
+ return m, true
+ default:
+ return "[" + reflect.TypeOf(v).String() + "]", false
+ }
+}
+
+// dumpVar helps to dump a variable in a template, to help debugging and development.
+func dumpVar(v any) template.HTML {
+ if setting.IsProd {
+ return "<pre>dumpVar: only available in dev mode</pre>"
+ }
+ m, ok := dumpVarMarshalable(v, map[uintptr]bool{})
+ dumpStr := ""
+ jsonBytes, err := json.MarshalIndent(m, "", " ")
+ if err != nil {
+ dumpStr = fmt.Sprintf("dumpVar: unable to marshal %T: %v", v, err)
+ } else if ok {
+ dumpStr = fmt.Sprintf("dumpVar: %T\n%s", v, string(jsonBytes))
+ } else {
+ dumpStr = fmt.Sprintf("dumpVar: unmarshalable %T\n%s", v, string(jsonBytes))
+ }
+ return template.HTML("<pre>" + html.EscapeString(dumpStr) + "</pre>")
+}
--- /dev/null
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package templates
+
+import (
+ "fmt"
+ "reflect"
+)
+
+type SliceUtils struct{}
+
+func NewSliceUtils() *SliceUtils {
+ return &SliceUtils{}
+}
+
+func (su *SliceUtils) Contains(s, v any) bool {
+ if s == nil {
+ return false
+ }
+ sv := reflect.ValueOf(s)
+ if sv.Kind() != reflect.Slice && sv.Kind() != reflect.Array {
+ panic(fmt.Sprintf("invalid type, expected slice or array, but got: %T", s))
+ }
+ for i := 0; i < sv.Len(); i++ {
+ it := sv.Index(i)
+ if !it.CanInterface() {
+ continue
+ }
+ if it.Interface() == v {
+ return true
+ }
+ }
+ return false
+}
--- /dev/null
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package templates
+
+import "strings"
+
+type StringUtils struct{}
+
+func NewStringUtils() *StringUtils {
+ return &StringUtils{}
+}
+
+func (su *StringUtils) HasPrefix(s, prefix string) bool {
+ return strings.HasPrefix(s, prefix)
+}
+
+func (su *StringUtils) Contains(s, substr string) bool {
+ return strings.Contains(s, substr)
+}
package templates
import (
+ "html/template"
+ "io"
+ "strings"
"testing"
"github.com/stretchr/testify/assert"
assert.Error(t, err)
}
}
+
+func TestUtils(t *testing.T) {
+ execTmpl := func(code string, data any) string {
+ tmpl := template.New("test")
+ tmpl.Funcs(template.FuncMap{"SliceUtils": NewSliceUtils, "StringUtils": NewStringUtils})
+ template.Must(tmpl.Parse(code))
+ w := &strings.Builder{}
+ assert.NoError(t, tmpl.Execute(w, data))
+ return w.String()
+ }
+
+ actual := execTmpl("{{SliceUtils.Contains .Slice .Value}}", map[string]any{"Slice": []string{"a", "b"}, "Value": "a"})
+ assert.Equal(t, "true", actual)
+
+ actual = execTmpl("{{SliceUtils.Contains .Slice .Value}}", map[string]any{"Slice": []string{"a", "b"}, "Value": "x"})
+ assert.Equal(t, "false", actual)
+
+ actual = execTmpl("{{SliceUtils.Contains .Slice .Value}}", map[string]any{"Slice": []int64{1, 2}, "Value": int64(2)})
+ assert.Equal(t, "true", actual)
+
+ actual = execTmpl("{{StringUtils.Contains .String .Value}}", map[string]any{"String": "abc", "Value": "b"})
+ assert.Equal(t, "true", actual)
+
+ actual = execTmpl("{{StringUtils.Contains .String .Value}}", map[string]any{"String": "abc", "Value": "x"})
+ assert.Equal(t, "false", actual)
+
+ tmpl := template.New("test")
+ tmpl.Funcs(template.FuncMap{"SliceUtils": NewSliceUtils, "StringUtils": NewStringUtils})
+ template.Must(tmpl.Parse("{{SliceUtils.Contains .Slice .Value}}"))
+ // error is like this: `template: test:1:12: executing "test" at <SliceUtils.Contains>: error calling Contains: ...`
+ err := tmpl.Execute(io.Discard, map[string]any{"Slice": struct{}{}})
+ assert.ErrorContains(t, err, "invalid type, expected slice or array")
+}
{{range $commit.Refs}}
{{$refGroup := .RefGroup}}
{{if eq $refGroup "pull"}}
- {{if or (not $.HidePRRefs) (containGeneric $.SelectedBranches .Name)}}
+ {{if or (not $.HidePRRefs) (SliceUtils.Contains $.SelectedBranches .Name)}}
<!-- it's intended to use issues not pulls, if it's a pull you will get redirected -->
<a class="ui labelled icon button basic tiny gt-mr-2" href="{{$.RepoLink}}/{{if $.Repository.UnitEnabled $.Context $.UnitTypePullRequests}}pulls{{else}}issues{{end}}/{{.ShortName|PathEscape}}">
{{svg "octicon-git-pull-request" 16 "gt-mr-2"}}#{{.ShortName}}
{{end}}
{{if or (.Permission.CanRead $.UnitTypeWiki) (.Permission.CanRead $.UnitTypeExternalWiki)}}
- <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}}>
+ <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}}>
{{svg "octicon-book"}} {{.locale.Tr "repo.wiki"}}
</a>
{{end}}
{{end}}
{{$previousExclusiveScope = $exclusiveScope}}
<div class="item issue-action" data-action="toggle" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/labels">
- {{if contain $.SelLabelIDs .ID}}{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}{{end}} {{RenderLabel $.Context .}}
+ {{if SliceUtils.Contains $.SelLabelIDs .ID}}{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}{{end}} {{RenderLabel $.Context .}}
</div>
{{end}}
</div>
<span class="info">{{.locale.Tr "repo.issues.filter_label_exclude" | Safe}}</span>
<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>
{{range .Labels}}
- <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>
+ <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>
{{end}}
</div>
</div>
<div class="menu">
{{range .Labels}}
<div class="item issue-action" data-action="toggle" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/labels">
- {{if contain $.SelLabelIDs .ID}}{{svg "octicon-check"}}{{end}} {{RenderLabel $.Context .}}
+ {{if SliceUtils.Contains $.SelLabelIDs .ID}}{{svg "octicon-check"}}{{end}} {{RenderLabel $.Context .}}
</div>
{{end}}
</div>
<div class="gt-f1 gt-p-3">
<a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}" title='{{$.ctxData.locale.Tr "repo.issues.attachment.open_tab" .Name}}'>
{{if FilenameIsImage .Name}}
- {{if not (containGeneric $.Content .UUID)}}
+ {{if not (StringUtils.Contains $.Content .UUID)}}
{{$hasThumbnails = true}}
{{end}}
{{svg "octicon-file"}}
<div class="ui small thumbnails">
{{- range .Attachments -}}
{{if FilenameIsImage .Name}}
- {{if not (containGeneric $.Content .UUID)}}
+ {{if not (StringUtils.Contains $.Content .UUID)}}
<a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}">
<img alt="{{.Name}}" src="{{.DownloadURL}}" title='{{$.ctxData.locale.Tr "repo.issues.attachment.open_tab" .Name}}'>
</a>
{{if or .AllowlistUserIDs (and $.Owner.IsOrganization .AllowlistTeamIDs)}}
{{$userIDs := .AllowlistUserIDs}}
{{range $.Users}}
- {{if contain $userIDs .ID}}
+ {{if SliceUtils.Contains $userIDs .ID}}
<a class="ui basic label" href="{{.HomeLink}}">{{avatar $.Context . 26}} {{.GetDisplayName}}</a>
{{end}}
{{end}}
{{if $.Owner.IsOrganization}}
{{$teamIDs := .AllowlistTeamIDs}}
{{range $.Teams}}
- {{if contain $teamIDs .ID}}
+ {{if SliceUtils.Contains $teamIDs .ID}}
<a class="ui basic label" href="{{$.Owner.OrganisationLink}}/teams/{{PathEscape .LowerName}}">{{.Name}}</a>
{{end}}
{{end}}