Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge template functions "dict/Dict/mergeinto" #23932

Merged
merged 6 commits into from Apr 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
92 changes: 5 additions & 87 deletions modules/templates/helper.go
Expand Up @@ -8,7 +8,6 @@ import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"html"
"html/template"
Expand Down Expand Up @@ -219,20 +218,6 @@ func NewFuncMap() []template.FuncMap {
"DisableImportLocal": func() bool {
return !setting.ImportLocalPaths
},
"Dict": func(values ...interface{}) (map[string]interface{}, error) {
if len(values)%2 != 0 {
return nil, errors.New("invalid dict call")
}
dict := make(map[string]interface{}, len(values)/2)
for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string)
if !ok {
return nil, errors.New("dict keys must be strings")
}
dict[key] = values[i+1]
}
return dict, nil
},
"Printf": fmt.Sprintf,
"Escape": Escape,
"Sec2Time": util.SecToTime,
Expand All @@ -242,35 +227,7 @@ func NewFuncMap() []template.FuncMap {
"DefaultTheme": func() string {
return setting.UI.DefaultTheme
},
// pass key-value pairs to a partial template which receives them as a dict
"dict": func(values ...interface{}) (map[string]interface{}, error) {
if len(values) == 0 {
return nil, errors.New("invalid dict call")
}

dict := make(map[string]interface{})
return util.MergeInto(dict, values...)
},
/* like dict but merge key-value pairs into the first dict and return it */
"mergeinto": func(root map[string]interface{}, values ...interface{}) (map[string]interface{}, error) {
if len(values) == 0 {
return nil, errors.New("invalid mergeinto call")
}

dict := make(map[string]interface{})
for key, value := range root {
dict[key] = value
}

return util.MergeInto(dict, values...)
},
"percentage": func(n int, values ...int) float32 {
sum := 0
for i := 0; i < len(values); i++ {
sum += values[i]
}
return float32(n) * 100 / float32(sum)
},
"dict": dict,
"CommentMustAsDiff": gitdiff.CommentMustAsDiff,
"MirrorRemoteAddress": mirrorRemoteAddress,
"NotificationSettings": func() map[string]interface{} {
Expand Down Expand Up @@ -413,52 +370,13 @@ func NewTextFuncMap() []texttmpl.FuncMap {
},
"EllipsisString": base.EllipsisString,
"URLJoin": util.URLJoin,
"Dict": func(values ...interface{}) (map[string]interface{}, error) {
if len(values)%2 != 0 {
return nil, errors.New("invalid dict call")
}
dict := make(map[string]interface{}, len(values)/2)
for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string)
if !ok {
return nil, errors.New("dict keys must be strings")
}
dict[key] = values[i+1]
}
return dict, nil
},
"Printf": fmt.Sprintf,
"Escape": Escape,
"Sec2Time": util.SecToTime,
"Printf": fmt.Sprintf,
"Escape": Escape,
"Sec2Time": util.SecToTime,
"ParseDeadline": func(deadline string) []string {
return strings.Split(deadline, "|")
},
"dict": func(values ...interface{}) (map[string]interface{}, error) {
if len(values) == 0 {
return nil, errors.New("invalid dict call")
}

dict := make(map[string]interface{})

for i := 0; i < len(values); i++ {
switch key := values[i].(type) {
case string:
i++
if i == len(values) {
return nil, errors.New("specify the key for non array values")
}
dict[key] = values[i]
case map[string]interface{}:
m := values[i].(map[string]interface{})
for i, v := range m {
dict[i] = v
}
default:
return nil, errors.New("dict values must be maps")
}
}
return dict, nil
},
"dict": dict,
"QueryEscape": url.QueryEscape,
"Eval": Eval,
}}
Expand Down
47 changes: 47 additions & 0 deletions modules/templates/util.go
@@ -0,0 +1,47 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package templates

import (
"fmt"
"reflect"
)

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
}
43 changes: 43 additions & 0 deletions modules/templates/util_test.go
@@ -0,0 +1,43 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package templates

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestDict(t *testing.T) {
type M map[string]any
cases := []struct {
args []any
want map[string]any
}{
{[]any{"a", 1, "b", 2}, M{"a": 1, "b": 2}},
{[]any{".", M{"base": 1}, "b", 2}, M{"base": 1, "b": 2}},
{[]any{"a", 1, ".", M{"extra": 2}}, M{"a": 1, "extra": 2}},
{[]any{"a", 1, ".", map[string]int{"int": 2}}, M{"a": 1, "int": 2}},
{[]any{".", nil, "b", 2}, M{"b": 2}},
}

for _, c := range cases {
got, err := dict(c.args...)
if assert.NoError(t, err) {
assert.EqualValues(t, c.want, got)
}
}

bads := []struct {
args []any
}{
{[]any{"a", 1, "b"}},
{[]any{1}},
{[]any{struct{}{}}},
}
for _, c := range bads {
_, err := dict(c.args...)
assert.Error(t, err)
}
}
24 changes: 0 additions & 24 deletions modules/util/util.go
Expand Up @@ -6,7 +6,6 @@ package util
import (
"bytes"
"crypto/rand"
"errors"
"fmt"
"math/big"
"strconv"
Expand Down Expand Up @@ -117,29 +116,6 @@ func NormalizeEOL(input []byte) []byte {
return tmp[:pos]
}

// MergeInto merges pairs of values into a "dict"
func MergeInto(dict map[string]interface{}, values ...interface{}) (map[string]interface{}, error) {
for i := 0; i < len(values); i++ {
switch key := values[i].(type) {
case string:
i++
if i == len(values) {
return nil, errors.New("specify the key for non array values")
}
dict[key] = values[i]
case map[string]interface{}:
m := values[i].(map[string]interface{})
for i, v := range m {
dict[i] = v
}
default:
return nil, errors.New("dict values must be maps")
}
}

return dict, nil
}

// CryptoRandomInt returns a crypto random integer between 0 and limit, inclusive
func CryptoRandomInt(limit int64) (int64, error) {
rInt, err := rand.Int(rand.Reader, big.NewInt(limit))
Expand Down
2 changes: 1 addition & 1 deletion templates/org/team/teams.tmpl
Expand Up @@ -32,7 +32,7 @@
</div>
<div class="ui attached segment members">
{{range .Members}}
{{template "shared/user/avatarlink" Dict "Context" $.Context "user" .}}
{{template "shared/user/avatarlink" dict "Context" $.Context "user" .}}
{{end}}
</div>
<div class="ui bottom attached header">
Expand Down
8 changes: 4 additions & 4 deletions templates/repo/diff/comments.tmpl
Expand Up @@ -5,7 +5,7 @@
{{if .OriginalAuthor}}
<span class="avatar"><img src="{{AppSubUrl}}/assets/img/avatar_default.png"></span>
{{else}}
{{template "shared/user/avatarlink" Dict "Context" $.root.Context "user" .Poster}}
{{template "shared/user/avatarlink" dict "Context" $.root.Context "user" .Poster}}
{{end}}
<div class="content comment-container">
<div class="ui top attached header comment-header gt-df gt-ac gt-sb">
Expand Down Expand Up @@ -42,8 +42,8 @@
</div>
{{end}}
{{end}}
{{template "repo/issue/view_content/add_reaction" Dict "ctxData" $.root "ActionURL" (Printf "%s/comments/%d/reactions" $.root.RepoLink .ID)}}
{{template "repo/issue/view_content/context_menu" Dict "ctxData" $.root "item" . "delete" true "issue" false "diff" true "IsCommentPoster" (and $.root.IsSigned (eq $.root.SignedUserID .PosterID))}}
{{template "repo/issue/view_content/add_reaction" dict "ctxData" $.root "ActionURL" (Printf "%s/comments/%d/reactions" $.root.RepoLink .ID)}}
{{template "repo/issue/view_content/context_menu" dict "ctxData" $.root "item" . "delete" true "issue" false "diff" true "IsCommentPoster" (and $.root.IsSigned (eq $.root.SignedUserID .PosterID))}}
</div>
</div>
<div class="ui attached segment comment-body">
Expand All @@ -60,7 +60,7 @@
{{$reactions := .Reactions.GroupByType}}
{{if $reactions}}
<div class="ui attached segment reactions">
{{template "repo/issue/view_content/reactions" Dict "ctxData" $.root "ActionURL" (Printf "%s/comments/%d/reactions" $.root.RepoLink .ID) "Reactions" $reactions}}
{{template "repo/issue/view_content/reactions" dict "ctxData" $.root "ActionURL" (Printf "%s/comments/%d/reactions" $.root.RepoLink .ID) "Reactions" $reactions}}
</div>
{{end}}
</div>
Expand Down
12 changes: 6 additions & 6 deletions templates/repo/diff/section_split.tmpl
Expand Up @@ -111,22 +111,22 @@
<td class="add-comment-left" colspan="4">
{{if gt (len $line.Comments) 0}}
{{if eq $line.GetCommentSide "previous"}}
{{template "repo/diff/conversation" mergeinto $.root "comments" $line.Comments}}
{{template "repo/diff/conversation" dict "." $.root "comments" $line.Comments}}
{{end}}
{{end}}
{{if gt (len $match.Comments) 0}}
{{if eq $match.GetCommentSide "previous"}}
{{template "repo/diff/conversation" mergeinto $.root "comments" $match.Comments}}
{{template "repo/diff/conversation" dict "." $.root "comments" $match.Comments}}
{{end}}
{{end}}
</td>
<td class="add-comment-right" colspan="4">
{{if eq $line.GetCommentSide "proposed"}}
{{template "repo/diff/conversation" mergeinto $.root "comments" $line.Comments}}
{{template "repo/diff/conversation" dict "." $.root "comments" $line.Comments}}
{{end}}
{{if gt (len $match.Comments) 0}}
{{if eq $match.GetCommentSide "proposed"}}
{{template "repo/diff/conversation" mergeinto $.root "comments" $match.Comments}}
{{template "repo/diff/conversation" dict "." $.root "comments" $match.Comments}}
{{end}}
{{end}}
</td>
Expand All @@ -137,13 +137,13 @@
<td class="add-comment-left" colspan="4">
{{if gt (len $line.Comments) 0}}
{{if eq $line.GetCommentSide "previous"}}
{{template "repo/diff/conversation" mergeinto $.root "comments" $line.Comments}}
{{template "repo/diff/conversation" dict "." $.root "comments" $line.Comments}}
{{end}}
{{end}}
</td>
<td class="add-comment-right" colspan="4">
{{if eq $line.GetCommentSide "proposed"}}
{{template "repo/diff/conversation" mergeinto $.root "comments" $line.Comments}}
{{template "repo/diff/conversation" dict "." $.root "comments" $line.Comments}}
{{end}}
</td>
</tr>
Expand Down
2 changes: 1 addition & 1 deletion templates/repo/diff/section_unified.tmpl
Expand Up @@ -57,7 +57,7 @@
{{if gt (len $line.Comments) 0}}
<tr class="add-comment" data-line-type="{{DiffLineTypeToStr .GetType}}">
<td class="add-comment-left add-comment-right" colspan="5">
{{template "repo/diff/conversation" mergeinto $.root "comments" $line.Comments}}
{{template "repo/diff/conversation" dict "." $.root "comments" $line.Comments}}
</td>
</tr>
{{end}}
Expand Down
2 changes: 1 addition & 1 deletion templates/repo/issue/list.tmpl
Expand Up @@ -307,7 +307,7 @@
</div>
</div>
</div>
{{template "shared/issuelist" mergeinto . "listType" "repo"}}
{{template "shared/issuelist" dict "." . "listType" "repo"}}
</div>
</div>
{{template "base/footer" .}}
2 changes: 1 addition & 1 deletion templates/repo/issue/milestone_issues.tmpl
Expand Up @@ -198,7 +198,7 @@
</div>
</div>
</div>
{{template "shared/issuelist" mergeinto . "listType" "milestone"}}
{{template "shared/issuelist" dict "." . "listType" "milestone"}}
</div>
</div>
{{template "base/footer" .}}
12 changes: 6 additions & 6 deletions templates/repo/issue/new_form.tmpl
Expand Up @@ -8,7 +8,7 @@
<div class="twelve wide column">
<div class="ui comments">
<div class="comment">
{{template "shared/user/avatarlink" Dict "Context" $.Context "user" .SignedUser}}
{{template "shared/user/avatarlink" dict "Context" $.Context "user" .SignedUser}}
<div class="ui segment content">
<div class="field">
<input name="title" id="issue_title" placeholder="{{.locale.Tr "repo.milestones.title"}}" value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}" tabindex="3" autofocus required maxlength="255" autocomplete="off">
Expand All @@ -20,15 +20,15 @@
<input type="hidden" name="template-file" value="{{.TemplateFile}}">
{{range .Fields}}
{{if eq .Type "input"}}
{{template "repo/issue/fields/input" Dict "Context" $.Context "item" .}}
{{template "repo/issue/fields/input" dict "Context" $.Context "item" .}}
{{else if eq .Type "markdown"}}
{{template "repo/issue/fields/markdown" Dict "Context" $.Context "item" .}}
{{template "repo/issue/fields/markdown" dict "Context" $.Context "item" .}}
{{else if eq .Type "textarea"}}
{{template "repo/issue/fields/textarea" Dict "Context" $.Context "item" .}}
{{template "repo/issue/fields/textarea" dict "Context" $.Context "item" .}}
{{else if eq .Type "dropdown"}}
{{template "repo/issue/fields/dropdown" Dict "Context" $.Context "item" .}}
{{template "repo/issue/fields/dropdown" dict "Context" $.Context "item" .}}
{{else if eq .Type "checkboxes"}}
{{template "repo/issue/fields/checkboxes" Dict "Context" $.Context "item" .}}
{{template "repo/issue/fields/checkboxes" dict "Context" $.Context "item" .}}
{{end}}
{{end}}
{{if .IsAttachmentEnabled}}
Expand Down