2018-09-29 02:33:54 -06:00
|
|
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
2014-04-10 12:20:58 -06:00
|
|
|
// Copyright 2014 The Gogs Authors. All rights reserved.
|
2022-11-27 11:20:29 -07:00
|
|
|
// SPDX-License-Identifier: MIT
|
2014-04-10 12:20:58 -06:00
|
|
|
|
2016-12-06 10:58:31 -07:00
|
|
|
package templates
|
2014-04-10 12:20:58 -06:00
|
|
|
|
|
|
|
import (
|
2017-03-01 17:25:44 -07:00
|
|
|
"bytes"
|
2022-01-19 16:26:57 -07:00
|
|
|
"context"
|
Scoped labels (#22585)
Add a new "exclusive" option per label. This makes it so that when the
label is named `scope/name`, no other label with the same `scope/`
prefix can be set on an issue.
The scope is determined by the last occurence of `/`, so for example
`scope/alpha/name` and `scope/beta/name` are considered to be in
different scopes and can coexist.
Exclusive scopes are not enforced by any database rules, however they
are enforced when editing labels at the models level, automatically
removing any existing labels in the same scope when either attaching a
new label or replacing all labels.
In menus use a circle instead of checkbox to indicate they function as
radio buttons per scope. Issue filtering by label ensures that only a
single scoped label is selected at a time. Clicking with alt key can be
used to remove a scoped label, both when editing individual issues and
batch editing.
Label rendering refactor for consistency and code simplification:
* Labels now consistently have the same shape, emojis and tooltips
everywhere. This includes the label list and label assignment menus.
* In label list, show description below label same as label menus.
* Don't use exactly black/white text colors to look a bit nicer.
* Simplify text color computation. There is no point computing luminance
in linear color space, as this is a perceptual problem and sRGB is
closer to perceptually linear.
* Increase height of label assignment menus to show more labels. Showing
only 3-4 labels at a time leads to a lot of scrolling.
* Render all labels with a new RenderLabel template helper function.
Label creation and editing in multiline modal menu:
* Change label creation to open a modal menu like label editing.
* Change menu layout to place name, description and colors on separate
lines.
* Don't color cancel button red in label editing modal menu.
* Align text to the left in model menu for better readability and
consistent with settings layout elsewhere.
Custom exclusive scoped label rendering:
* Display scoped label prefix and suffix with slightly darker and
lighter background color respectively, and a slanted edge between them
similar to the `/` symbol.
* In menus exclusive labels are grouped with a divider line.
---------
Co-authored-by: Yarden Shoham <hrsi88@gmail.com>
Co-authored-by: Lauris BH <lauris@nix.lv>
2023-02-18 12:17:39 -07:00
|
|
|
"encoding/hex"
|
2014-04-10 12:20:58 -06:00
|
|
|
"fmt"
|
2018-02-27 00:09:18 -07:00
|
|
|
"html"
|
2014-04-10 12:20:58 -06:00
|
|
|
"html/template"
|
Scoped labels (#22585)
Add a new "exclusive" option per label. This makes it so that when the
label is named `scope/name`, no other label with the same `scope/`
prefix can be set on an issue.
The scope is determined by the last occurence of `/`, so for example
`scope/alpha/name` and `scope/beta/name` are considered to be in
different scopes and can coexist.
Exclusive scopes are not enforced by any database rules, however they
are enforced when editing labels at the models level, automatically
removing any existing labels in the same scope when either attaching a
new label or replacing all labels.
In menus use a circle instead of checkbox to indicate they function as
radio buttons per scope. Issue filtering by label ensures that only a
single scoped label is selected at a time. Clicking with alt key can be
used to remove a scoped label, both when editing individual issues and
batch editing.
Label rendering refactor for consistency and code simplification:
* Labels now consistently have the same shape, emojis and tooltips
everywhere. This includes the label list and label assignment menus.
* In label list, show description below label same as label menus.
* Don't use exactly black/white text colors to look a bit nicer.
* Simplify text color computation. There is no point computing luminance
in linear color space, as this is a perceptual problem and sRGB is
closer to perceptually linear.
* Increase height of label assignment menus to show more labels. Showing
only 3-4 labels at a time leads to a lot of scrolling.
* Render all labels with a new RenderLabel template helper function.
Label creation and editing in multiline modal menu:
* Change label creation to open a modal menu like label editing.
* Change menu layout to place name, description and colors on separate
lines.
* Don't color cancel button red in label editing modal menu.
* Align text to the left in model menu for better readability and
consistent with settings layout elsewhere.
Custom exclusive scoped label rendering:
* Display scoped label prefix and suffix with slightly darker and
lighter background color respectively, and a slanted edge between them
similar to the `/` symbol.
* In menus exclusive labels are grouped with a divider line.
---------
Co-authored-by: Yarden Shoham <hrsi88@gmail.com>
Co-authored-by: Lauris BH <lauris@nix.lv>
2023-02-18 12:17:39 -07:00
|
|
|
"math"
|
2016-08-11 17:16:36 -06:00
|
|
|
"mime"
|
2017-11-28 02:43:51 -07:00
|
|
|
"net/url"
|
2016-08-11 17:16:36 -06:00
|
|
|
"path/filepath"
|
2020-11-08 10:21:54 -07:00
|
|
|
"reflect"
|
2019-11-07 06:34:28 -07:00
|
|
|
"regexp"
|
2014-04-10 12:20:58 -06:00
|
|
|
"strings"
|
|
|
|
"time"
|
2019-10-31 22:48:30 -06:00
|
|
|
"unicode"
|
2014-05-25 18:11:25 -06:00
|
|
|
|
2022-08-24 20:31:57 -06:00
|
|
|
activities_model "code.gitea.io/gitea/models/activities"
|
Avatar refactor, move avatar code from `models` to `models.avatars`, remove duplicated code (#17123)
Why this refactor
The goal is to move most files from `models` package to `models.xxx` package. Many models depend on avatar model, so just move this first.
And the existing logic is not clear, there are too many function like `AvatarLink`, `RelAvatarLink`, `SizedRelAvatarLink`, `SizedAvatarLink`, `MakeFinalAvatarURL`, `HashedAvatarLink`, etc. This refactor make everything clear:
* user.AvatarLink()
* user.AvatarLinkWithSize(size)
* avatars.GenerateEmailAvatarFastLink(email, size)
* avatars.GenerateEmailAvatarFinalLink(email, size)
And many duplicated code are deleted in route handler, the handler and the model share the same avatar logic now.
2021-10-05 17:25:46 -06:00
|
|
|
"code.gitea.io/gitea/models/avatars"
|
2022-06-13 03:37:59 -06:00
|
|
|
issues_model "code.gitea.io/gitea/models/issues"
|
2022-03-29 00:29:02 -06:00
|
|
|
"code.gitea.io/gitea/models/organization"
|
2021-12-09 18:27:50 -07:00
|
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
2022-10-16 17:29:26 -06:00
|
|
|
system_model "code.gitea.io/gitea/models/system"
|
2021-11-24 02:49:20 -07:00
|
|
|
user_model "code.gitea.io/gitea/models/user"
|
2016-11-10 09:24:48 -07:00
|
|
|
"code.gitea.io/gitea/modules/base"
|
2020-04-28 12:05:39 -06:00
|
|
|
"code.gitea.io/gitea/modules/emoji"
|
2021-06-14 11:20:43 -06:00
|
|
|
"code.gitea.io/gitea/modules/git"
|
2022-06-11 07:50:14 -06:00
|
|
|
giturl "code.gitea.io/gitea/modules/git/url"
|
2022-11-08 08:13:58 -07:00
|
|
|
gitea_html "code.gitea.io/gitea/modules/html"
|
2021-07-24 10:03:58 -06:00
|
|
|
"code.gitea.io/gitea/modules/json"
|
2016-11-10 09:24:48 -07:00
|
|
|
"code.gitea.io/gitea/modules/log"
|
2017-09-16 11:17:57 -06:00
|
|
|
"code.gitea.io/gitea/modules/markup"
|
2022-03-30 02:42:47 -06:00
|
|
|
"code.gitea.io/gitea/modules/markup/markdown"
|
2020-01-10 02:34:21 -07:00
|
|
|
"code.gitea.io/gitea/modules/repository"
|
2016-11-10 09:24:48 -07:00
|
|
|
"code.gitea.io/gitea/modules/setting"
|
2020-07-12 03:10:56 -06:00
|
|
|
"code.gitea.io/gitea/modules/svg"
|
Use a general Eval function for expressions in templates. (#23927)
One of the proposals in #23328
This PR introduces a simple expression calculator
(templates/eval/eval.go), it can do basic expression calculations.
Many untested template helper functions like `Mul` `Add` can be replaced
by this new approach.
Then these `Add` / `Mul` / `percentage` / `Subtract` / `DiffStatsWidth`
could all use this `Eval`.
And it provides enhancements for Golang templates, and improves
readability.
Some examples:
----
* Before: `{{Add (Mul $glyph.Row 12) 12}}`
* After: `{{Eval $glyph.Row "*" 12 "+" 12}}`
----
* Before: `{{if lt (Add $i 1) (len $.Topics)}}`
* After: `{{if Eval $i "+" 1 "<" (len $.Topics)}}`
## FAQ
### Why not use an existing expression package?
We need a highly customized expression engine:
* do the calculation on the fly, without pre-compiling
* deal with int/int64/float64 types, to make the result could be used in
Golang template.
* make the syntax could be used in the Golang template directly
* do not introduce too much complex or strange syntax, we just need a
simple calculator.
* it needs to strictly follow Golang template's behavior, for example,
Golang template treats all non-zero values as truth, but many 3rd
packages don't do so.
### What's the benefit?
* Developers don't need to add more `Add`/`Mul`/`Sub`-like functions,
they were getting more and more.
Now, only one `Eval` is enough for all cases.
* The new code reads better than old `{{Add (Mul $glyph.Row 12) 12}}`,
the old one isn't familiar to most procedural programming developers
(eg, the Golang expression syntax).
* The `Eval` is fully covered by tests, many old `Add`/`Mul`-like
functions were never tested.
### The performance?
It doesn't use `reflect`, it doesn't need to parse or compile when used
in Golang template, the performance is as fast as native Go template.
### Is it too complex? Could it be unstable?
The expression calculator program is a common homework for computer
science students, and it's widely used as a teaching and practicing
purpose for developers. The algorithm is pretty well-known.
The behavior can be clearly defined, it is stable.
2023-04-07 07:25:49 -06:00
|
|
|
"code.gitea.io/gitea/modules/templates/eval"
|
2019-08-15 08:46:21 -06:00
|
|
|
"code.gitea.io/gitea/modules/timeutil"
|
|
|
|
"code.gitea.io/gitea/modules/util"
|
2019-09-05 20:20:09 -06:00
|
|
|
"code.gitea.io/gitea/services/gitdiff"
|
2017-11-22 00:09:48 -07:00
|
|
|
|
2019-10-15 15:24:16 -06:00
|
|
|
"github.com/editorconfig/editorconfig-core-go/v2"
|
2014-04-10 12:20:58 -06:00
|
|
|
)
|
|
|
|
|
2019-11-07 06:34:28 -07:00
|
|
|
// Used from static.go && dynamic.go
|
|
|
|
var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}[\s]*$`)
|
|
|
|
|
2016-11-24 23:23:48 -07:00
|
|
|
// NewFuncMap returns functions for injecting to templates
|
2016-03-06 14:40:04 -07:00
|
|
|
func NewFuncMap() []template.FuncMap {
|
|
|
|
return []template.FuncMap{map[string]interface{}{
|
2023-04-08 07:15:22 -06:00
|
|
|
// -----------------------------------------------------------------
|
|
|
|
// html/template related functions
|
|
|
|
"dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
|
|
|
|
"Eval": Eval,
|
|
|
|
"Safe": Safe,
|
|
|
|
"Escape": html.EscapeString,
|
|
|
|
"QueryEscape": url.QueryEscape,
|
|
|
|
"JSEscape": template.JSEscapeString,
|
|
|
|
"Str2html": Str2html, // TODO: rename it to SanitizeHTML
|
|
|
|
"URLJoin": util.URLJoin,
|
|
|
|
|
|
|
|
"PathEscape": url.PathEscape,
|
|
|
|
"PathEscapeSegments": util.PathEscapeSegments,
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------
|
|
|
|
// string / json
|
|
|
|
"Join": strings.Join,
|
|
|
|
"DotEscape": DotEscape,
|
|
|
|
"HasPrefix": strings.HasPrefix,
|
|
|
|
"EllipsisString": base.EllipsisString,
|
Add `DumpVar` helper function to help debugging templates (#24262)
I guess many contributors might agree that it's really difficult to
write Golang template. The dot syntax `.` confuses everyone: what
variable it is ....
So, we can use a `{{DumpVar .ContextUser}}` to look into every variable
now.
![image](https://user-images.githubusercontent.com/2114189/233692383-f3c8f24d-4465-45f8-839b-b63e00731559.png)
And it can even dump the whole `ctx.Data` by `{{DumpVar .}}`:
```
dumpVar: templates.Vars
{
"AllLangs": [
{
"Lang": "id-ID",
"Name": "Bahasa Indonesia"
},
...
"Context": "[dumped]",
"ContextUser": {
"AllowCreateOrganization": true,
"AllowGitHook": false,
"AllowImportLocal": false,
...
"TemplateLoadTimes": "[func() string]",
"TemplateName": "user/profile",
"Title": "Full'\u003cspan\u003e Name",
"Total": 7,
"UnitActionsGlobalDisabled": false,
"UnitIssuesGlobalDisabled": false,
"UnitProjectsGlobalDisabled": false,
"UnitPullsGlobalDisabled": false,
"UnitWikiGlobalDisabled": false,
"locale": {
"Lang": "en-US",
"LangName": "English",
"Locale": {}
}
...
---------
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: silverwind <me@silverwind.io>
2023-04-22 11:28:20 -06:00
|
|
|
"DumpVar": dumpVar,
|
2023-04-08 07:15:22 -06:00
|
|
|
|
|
|
|
"Json": func(in interface{}) string {
|
|
|
|
out, err := json.Marshal(in)
|
|
|
|
if err != nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return string(out)
|
|
|
|
},
|
|
|
|
"JsonPrettyPrint": func(in string) string {
|
|
|
|
var out bytes.Buffer
|
|
|
|
err := json.Indent(&out, []byte(in), "", " ")
|
|
|
|
if err != nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return out.String()
|
|
|
|
},
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------
|
|
|
|
// svg / avatar / icon
|
|
|
|
"svg": svg.RenderHTML,
|
|
|
|
"avatar": Avatar,
|
|
|
|
"avatarHTML": AvatarHTML,
|
|
|
|
"avatarByAction": AvatarByAction,
|
|
|
|
"avatarByEmail": AvatarByEmail,
|
|
|
|
"repoAvatar": RepoAvatar,
|
|
|
|
"EntryIcon": base.EntryIcon,
|
|
|
|
"MigrationIcon": MigrationIcon,
|
|
|
|
"ActionIcon": ActionIcon,
|
|
|
|
|
|
|
|
"SortArrow": func(normSort, revSort, urlSort string, isDefault bool) template.HTML {
|
|
|
|
// if needed
|
|
|
|
if len(normSort) == 0 || len(urlSort) == 0 {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(urlSort) == 0 && isDefault {
|
|
|
|
// if sort is sorted as default add arrow tho this table header
|
|
|
|
if isDefault {
|
|
|
|
return svg.RenderHTML("octicon-triangle-down", 16)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// if sort arg is in url test if it correlates with column header sort arguments
|
|
|
|
// the direction of the arrow should indicate the "current sort order", up means ASC(normal), down means DESC(rev)
|
|
|
|
if urlSort == normSort {
|
|
|
|
// the table is sorted with this header normal
|
|
|
|
return svg.RenderHTML("octicon-triangle-up", 16)
|
|
|
|
} else if urlSort == revSort {
|
|
|
|
// the table is sorted with this header reverse
|
|
|
|
return svg.RenderHTML("octicon-triangle-down", 16)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// the table is NOT sorted with this header
|
|
|
|
return ""
|
|
|
|
},
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------
|
|
|
|
// time / number / format
|
|
|
|
"FileSize": base.FileSize,
|
|
|
|
"CountFmt": base.FormatNumberSI,
|
|
|
|
"TimeSince": timeutil.TimeSince,
|
|
|
|
"TimeSinceUnix": timeutil.TimeSinceUnix,
|
|
|
|
"Sec2Time": util.SecToTime,
|
|
|
|
"DateFmtLong": func(t time.Time) string {
|
2023-04-10 17:01:20 -06:00
|
|
|
return t.Format(time.RFC3339)
|
2023-04-08 07:15:22 -06:00
|
|
|
},
|
|
|
|
"LoadTimes": func(startTime time.Time) string {
|
|
|
|
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
|
2016-03-06 14:40:04 -07:00
|
|
|
"AppName": func() string {
|
|
|
|
return setting.AppName
|
|
|
|
},
|
|
|
|
"AppSubUrl": func() string {
|
2016-11-27 03:14:25 -07:00
|
|
|
return setting.AppSubURL
|
2016-03-06 14:40:04 -07:00
|
|
|
},
|
2021-05-08 08:27:25 -06:00
|
|
|
"AssetUrlPrefix": func() string {
|
2021-04-28 06:35:06 -06:00
|
|
|
return setting.StaticURLPrefix + "/assets"
|
2019-10-22 06:11:01 -06:00
|
|
|
},
|
2016-03-06 14:40:04 -07:00
|
|
|
"AppUrl": func() string {
|
2023-02-09 09:31:30 -07:00
|
|
|
// The usage of AppUrl should be avoided as much as possible,
|
|
|
|
// because the AppURL(ROOT_URL) may not match user's visiting site and the ROOT_URL in app.ini may be incorrect.
|
|
|
|
// And it's difficult for Gitea to guess absolute URL correctly with zero configuration,
|
|
|
|
// because Gitea doesn't know whether the scheme is HTTP or HTTPS unless the reverse proxy could tell Gitea.
|
2016-11-27 03:14:25 -07:00
|
|
|
return setting.AppURL
|
2016-03-06 14:40:04 -07:00
|
|
|
},
|
|
|
|
"AppVer": func() string {
|
|
|
|
return setting.AppVer
|
|
|
|
},
|
2023-04-07 01:31:41 -06:00
|
|
|
"AppDomain": func() string { // documented in mail-templates.md
|
2016-03-06 14:40:04 -07:00
|
|
|
return setting.Domain
|
|
|
|
},
|
2022-08-23 06:58:04 -06:00
|
|
|
"AssetVersion": func() string {
|
|
|
|
return setting.AssetVersion
|
|
|
|
},
|
Add context cache as a request level cache (#22294)
To avoid duplicated load of the same data in an HTTP request, we can set
a context cache to do that. i.e. Some pages may load a user from a
database with the same id in different areas on the same page. But the
code is hidden in two different deep logic. How should we share the
user? As a result of this PR, now if both entry functions accept
`context.Context` as the first parameter and we just need to refactor
`GetUserByID` to reuse the user from the context cache. Then it will not
be loaded twice on an HTTP request.
But of course, sometimes we would like to reload an object from the
database, that's why `RemoveContextData` is also exposed.
The core context cache is here. It defines a new context
```go
type cacheContext struct {
ctx context.Context
data map[any]map[any]any
lock sync.RWMutex
}
var cacheContextKey = struct{}{}
func WithCacheContext(ctx context.Context) context.Context {
return context.WithValue(ctx, cacheContextKey, &cacheContext{
ctx: ctx,
data: make(map[any]map[any]any),
})
}
```
Then you can use the below 4 methods to read/write/del the data within
the same context.
```go
func GetContextData(ctx context.Context, tp, key any) any
func SetContextData(ctx context.Context, tp, key, value any)
func RemoveContextData(ctx context.Context, tp, key any)
func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error)
```
Then let's take a look at how `system.GetString` implement it.
```go
func GetSetting(ctx context.Context, key string) (string, error) {
return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
res, err := GetSettingNoCache(ctx, key)
if err != nil {
return "", err
}
return res.SettingValue, nil
})
})
}
```
First, it will check if context data include the setting object with the
key. If not, it will query from the global cache which may be memory or
a Redis cache. If not, it will get the object from the database. In the
end, if the object gets from the global cache or database, it will be
set into the context cache.
An object stored in the context cache will only be destroyed after the
context disappeared.
2023-02-15 06:37:34 -07:00
|
|
|
"DisableGravatar": func(ctx context.Context) bool {
|
2023-02-24 03:23:13 -07:00
|
|
|
return system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar)
|
2016-03-06 14:40:04 -07:00
|
|
|
},
|
2019-05-08 02:41:35 -06:00
|
|
|
"DefaultShowFullName": func() bool {
|
|
|
|
return setting.UI.DefaultShowFullName
|
|
|
|
},
|
2016-08-31 23:01:32 -06:00
|
|
|
"ShowFooterTemplateLoadTime": func() bool {
|
|
|
|
return setting.ShowFooterTemplateLoadTime
|
|
|
|
},
|
2019-12-27 16:43:56 -07:00
|
|
|
"AllowedReactions": func() []string {
|
|
|
|
return setting.UI.Reactions
|
|
|
|
},
|
2021-06-29 08:28:38 -06:00
|
|
|
"CustomEmojis": func() map[string]string {
|
|
|
|
return setting.UI.CustomEmojisMap
|
|
|
|
},
|
2016-03-06 14:40:04 -07:00
|
|
|
"ThemeColorMetaTag": func() string {
|
2016-07-23 10:23:54 -06:00
|
|
|
return setting.UI.ThemeColorMetaTag
|
2016-03-06 14:40:04 -07:00
|
|
|
},
|
2017-03-31 19:03:01 -06:00
|
|
|
"MetaAuthor": func() string {
|
|
|
|
return setting.UI.Meta.Author
|
|
|
|
},
|
|
|
|
"MetaDescription": func() string {
|
|
|
|
return setting.UI.Meta.Description
|
|
|
|
},
|
|
|
|
"MetaKeywords": func() string {
|
|
|
|
return setting.UI.Meta.Keywords
|
|
|
|
},
|
2019-11-21 13:06:23 -07:00
|
|
|
"UseServiceWorker": func() bool {
|
|
|
|
return setting.UI.UseServiceWorker
|
|
|
|
},
|
2021-02-19 16:06:56 -07:00
|
|
|
"EnableTimetracking": func() bool {
|
|
|
|
return setting.Service.EnableTimetracking
|
|
|
|
},
|
2017-09-12 03:25:42 -06:00
|
|
|
"DisableGitHooks": func() bool {
|
|
|
|
return setting.DisableGitHooks
|
|
|
|
},
|
2021-02-11 10:34:34 -07:00
|
|
|
"DisableWebhooks": func() bool {
|
|
|
|
return setting.DisableWebhooks
|
|
|
|
},
|
2018-08-23 23:00:22 -06:00
|
|
|
"DisableImportLocal": func() bool {
|
|
|
|
return !setting.ImportLocalPaths
|
|
|
|
},
|
2018-07-05 15:25:04 -06:00
|
|
|
"DefaultTheme": func() string {
|
|
|
|
return setting.UI.DefaultTheme
|
|
|
|
},
|
2020-07-03 03:55:36 -06:00
|
|
|
"NotificationSettings": func() map[string]interface{} {
|
|
|
|
return map[string]interface{}{
|
2020-05-07 15:49:00 -06:00
|
|
|
"MinTimeout": int(setting.UI.Notification.MinTimeout / time.Millisecond),
|
|
|
|
"TimeoutStep": int(setting.UI.Notification.TimeoutStep / time.Millisecond),
|
|
|
|
"MaxTimeout": int(setting.UI.Notification.MaxTimeout / time.Millisecond),
|
|
|
|
"EventSourceUpdateTime": int(setting.UI.Notification.EventSourceUpdateTime / time.Millisecond),
|
2020-04-23 21:57:38 -06:00
|
|
|
}
|
|
|
|
},
|
2023-04-08 07:15:22 -06:00
|
|
|
"MermaidMaxSourceCharacters": func() int {
|
|
|
|
return setting.MermaidMaxSourceCharacters
|
|
|
|
},
|
2020-11-08 10:21:54 -07:00
|
|
|
|
2023-04-08 07:15:22 -06:00
|
|
|
// -----------------------------------------------------------------
|
|
|
|
// render
|
|
|
|
"RenderCommitMessage": RenderCommitMessage,
|
|
|
|
"RenderCommitMessageLinkSubject": RenderCommitMessageLinkSubject,
|
2020-11-08 10:21:54 -07:00
|
|
|
|
2023-04-08 07:15:22 -06:00
|
|
|
"RenderCommitBody": RenderCommitBody,
|
|
|
|
"RenderCodeBlock": RenderCodeBlock,
|
|
|
|
"RenderIssueTitle": RenderIssueTitle,
|
|
|
|
"RenderEmoji": RenderEmoji,
|
|
|
|
"RenderEmojiPlain": emoji.ReplaceAliases,
|
|
|
|
"ReactionToEmoji": ReactionToEmoji,
|
|
|
|
"RenderNote": RenderNote,
|
2020-06-24 16:23:05 -06:00
|
|
|
|
2023-04-08 07:15:22 -06:00
|
|
|
"RenderMarkdownToHtml": func(ctx context.Context, input string) template.HTML {
|
|
|
|
output, err := markdown.RenderString(&markup.RenderContext{
|
|
|
|
Ctx: ctx,
|
|
|
|
URLPrefix: setting.AppSubURL,
|
|
|
|
}, input)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("RenderString: %v", err)
|
2020-06-24 16:23:05 -06:00
|
|
|
}
|
2023-04-08 07:15:22 -06:00
|
|
|
return template.HTML(output)
|
2020-02-11 10:02:41 -07:00
|
|
|
},
|
2023-03-05 14:59:05 -07:00
|
|
|
"RenderLabel": func(ctx context.Context, label *issues_model.Label) template.HTML {
|
|
|
|
return template.HTML(RenderLabel(ctx, label))
|
Scoped labels (#22585)
Add a new "exclusive" option per label. This makes it so that when the
label is named `scope/name`, no other label with the same `scope/`
prefix can be set on an issue.
The scope is determined by the last occurence of `/`, so for example
`scope/alpha/name` and `scope/beta/name` are considered to be in
different scopes and can coexist.
Exclusive scopes are not enforced by any database rules, however they
are enforced when editing labels at the models level, automatically
removing any existing labels in the same scope when either attaching a
new label or replacing all labels.
In menus use a circle instead of checkbox to indicate they function as
radio buttons per scope. Issue filtering by label ensures that only a
single scoped label is selected at a time. Clicking with alt key can be
used to remove a scoped label, both when editing individual issues and
batch editing.
Label rendering refactor for consistency and code simplification:
* Labels now consistently have the same shape, emojis and tooltips
everywhere. This includes the label list and label assignment menus.
* In label list, show description below label same as label menus.
* Don't use exactly black/white text colors to look a bit nicer.
* Simplify text color computation. There is no point computing luminance
in linear color space, as this is a perceptual problem and sRGB is
closer to perceptually linear.
* Increase height of label assignment menus to show more labels. Showing
only 3-4 labels at a time leads to a lot of scrolling.
* Render all labels with a new RenderLabel template helper function.
Label creation and editing in multiline modal menu:
* Change label creation to open a modal menu like label editing.
* Change menu layout to place name, description and colors on separate
lines.
* Don't color cancel button red in label editing modal menu.
* Align text to the left in model menu for better readability and
consistent with settings layout elsewhere.
Custom exclusive scoped label rendering:
* Display scoped label prefix and suffix with slightly darker and
lighter background color respectively, and a slanted edge between them
similar to the `/` symbol.
* In menus exclusive labels are grouped with a divider line.
---------
Co-authored-by: Yarden Shoham <hrsi88@gmail.com>
Co-authored-by: Lauris BH <lauris@nix.lv>
2023-02-18 12:17:39 -07:00
|
|
|
},
|
2023-03-05 14:59:05 -07:00
|
|
|
"RenderLabels": func(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML {
|
2022-09-16 06:44:00 -06:00
|
|
|
htmlCode := `<span class="labels-list">`
|
2020-10-25 15:49:48 -06:00
|
|
|
for _, label := range labels {
|
2021-02-09 19:50:44 -07:00
|
|
|
// Protect against nil value in labels - shouldn't happen but would cause a panic if so
|
|
|
|
if label == nil {
|
|
|
|
continue
|
|
|
|
}
|
Scoped labels (#22585)
Add a new "exclusive" option per label. This makes it so that when the
label is named `scope/name`, no other label with the same `scope/`
prefix can be set on an issue.
The scope is determined by the last occurence of `/`, so for example
`scope/alpha/name` and `scope/beta/name` are considered to be in
different scopes and can coexist.
Exclusive scopes are not enforced by any database rules, however they
are enforced when editing labels at the models level, automatically
removing any existing labels in the same scope when either attaching a
new label or replacing all labels.
In menus use a circle instead of checkbox to indicate they function as
radio buttons per scope. Issue filtering by label ensures that only a
single scoped label is selected at a time. Clicking with alt key can be
used to remove a scoped label, both when editing individual issues and
batch editing.
Label rendering refactor for consistency and code simplification:
* Labels now consistently have the same shape, emojis and tooltips
everywhere. This includes the label list and label assignment menus.
* In label list, show description below label same as label menus.
* Don't use exactly black/white text colors to look a bit nicer.
* Simplify text color computation. There is no point computing luminance
in linear color space, as this is a perceptual problem and sRGB is
closer to perceptually linear.
* Increase height of label assignment menus to show more labels. Showing
only 3-4 labels at a time leads to a lot of scrolling.
* Render all labels with a new RenderLabel template helper function.
Label creation and editing in multiline modal menu:
* Change label creation to open a modal menu like label editing.
* Change menu layout to place name, description and colors on separate
lines.
* Don't color cancel button red in label editing modal menu.
* Align text to the left in model menu for better readability and
consistent with settings layout elsewhere.
Custom exclusive scoped label rendering:
* Display scoped label prefix and suffix with slightly darker and
lighter background color respectively, and a slanted edge between them
similar to the `/` symbol.
* In menus exclusive labels are grouped with a divider line.
---------
Co-authored-by: Yarden Shoham <hrsi88@gmail.com>
Co-authored-by: Lauris BH <lauris@nix.lv>
2023-02-18 12:17:39 -07:00
|
|
|
htmlCode += fmt.Sprintf("<a href='%s/issues?labels=%d'>%s</a> ",
|
2023-03-05 14:59:05 -07:00
|
|
|
repoLink, label.ID, RenderLabel(ctx, label))
|
2020-10-25 15:49:48 -06:00
|
|
|
}
|
2022-09-16 06:44:00 -06:00
|
|
|
htmlCode += "</span>"
|
|
|
|
return template.HTML(htmlCode)
|
2020-10-25 15:49:48 -06:00
|
|
|
},
|
2023-04-08 07:15:22 -06:00
|
|
|
|
|
|
|
// -----------------------------------------------------------------
|
|
|
|
// misc
|
|
|
|
"DiffLineTypeToStr": DiffLineTypeToStr,
|
|
|
|
"ShortSha": base.ShortSha,
|
|
|
|
"ActionContent2Commits": ActionContent2Commits,
|
|
|
|
"IsMultilineCommitMessage": IsMultilineCommitMessage,
|
|
|
|
"CommentMustAsDiff": gitdiff.CommentMustAsDiff,
|
|
|
|
"MirrorRemoteAddress": mirrorRemoteAddress,
|
|
|
|
|
|
|
|
"ParseDeadline": func(deadline string) []string {
|
|
|
|
return strings.Split(deadline, "|")
|
2021-07-23 22:21:51 -06:00
|
|
|
},
|
2023-04-08 07:15:22 -06:00
|
|
|
"FilenameIsImage": func(filename string) bool {
|
|
|
|
mimeType := mime.TypeByExtension(filepath.Ext(filename))
|
|
|
|
return strings.HasPrefix(mimeType, "image/")
|
|
|
|
},
|
|
|
|
"TabSizeClass": func(ec interface{}, filename string) string {
|
|
|
|
var (
|
|
|
|
value *editorconfig.Editorconfig
|
|
|
|
ok bool
|
|
|
|
)
|
|
|
|
if ec != nil {
|
|
|
|
if value, ok = ec.(*editorconfig.Editorconfig); !ok || value == nil {
|
|
|
|
return "tab-size-8"
|
|
|
|
}
|
|
|
|
def, err := value.GetDefinitionForFilename(filename)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("tab size class: getting definition for filename: %v", err)
|
|
|
|
return "tab-size-8"
|
|
|
|
}
|
|
|
|
if def.TabWidth > 0 {
|
|
|
|
return fmt.Sprintf("tab-size-%d", def.TabWidth)
|
|
|
|
}
|
2022-06-08 02:59:16 -06:00
|
|
|
}
|
2023-04-08 07:15:22 -06:00
|
|
|
return "tab-size-8"
|
|
|
|
},
|
|
|
|
"SubJumpablePath": func(str string) []string {
|
|
|
|
var path []string
|
|
|
|
index := strings.LastIndex(str, "/")
|
|
|
|
if index != -1 && index != len(str) {
|
|
|
|
path = append(path, str[0:index+1], str[index+1:])
|
|
|
|
} else {
|
|
|
|
path = append(path, str)
|
|
|
|
}
|
|
|
|
return path
|
2022-06-08 02:59:16 -06:00
|
|
|
},
|
2022-10-21 02:39:26 -06:00
|
|
|
"CompareLink": func(baseRepo, repo *repo_model.Repository, branchName string) string {
|
|
|
|
var curBranch string
|
|
|
|
if repo.ID != baseRepo.ID {
|
|
|
|
curBranch += fmt.Sprintf("%s/%s:", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name))
|
|
|
|
}
|
|
|
|
curBranch += util.PathEscapeSegments(branchName)
|
|
|
|
|
|
|
|
return fmt.Sprintf("%s/compare/%s...%s",
|
|
|
|
baseRepo.Link(),
|
|
|
|
util.PathEscapeSegments(baseRepo.DefaultBranch),
|
|
|
|
curBranch,
|
|
|
|
)
|
|
|
|
},
|
2019-11-07 06:34:28 -07:00
|
|
|
}}
|
|
|
|
}
|
|
|
|
|
2020-12-08 17:12:15 -07:00
|
|
|
// AvatarHTML creates the HTML for an avatar
|
2021-12-19 21:41:31 -07:00
|
|
|
func AvatarHTML(src string, size int, class, name string) template.HTML {
|
2020-12-03 11:46:11 -07:00
|
|
|
sizeStr := fmt.Sprintf(`%d`, size)
|
|
|
|
|
|
|
|
if name == "" {
|
|
|
|
name = "avatar"
|
2020-09-08 11:17:56 -06:00
|
|
|
}
|
|
|
|
|
2020-12-03 11:46:11 -07:00
|
|
|
return template.HTML(`<img class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`)
|
|
|
|
}
|
|
|
|
|
2020-12-07 21:14:28 -07:00
|
|
|
// Avatar renders user avatars. args: user, size (int), class (string)
|
Add context cache as a request level cache (#22294)
To avoid duplicated load of the same data in an HTTP request, we can set
a context cache to do that. i.e. Some pages may load a user from a
database with the same id in different areas on the same page. But the
code is hidden in two different deep logic. How should we share the
user? As a result of this PR, now if both entry functions accept
`context.Context` as the first parameter and we just need to refactor
`GetUserByID` to reuse the user from the context cache. Then it will not
be loaded twice on an HTTP request.
But of course, sometimes we would like to reload an object from the
database, that's why `RemoveContextData` is also exposed.
The core context cache is here. It defines a new context
```go
type cacheContext struct {
ctx context.Context
data map[any]map[any]any
lock sync.RWMutex
}
var cacheContextKey = struct{}{}
func WithCacheContext(ctx context.Context) context.Context {
return context.WithValue(ctx, cacheContextKey, &cacheContext{
ctx: ctx,
data: make(map[any]map[any]any),
})
}
```
Then you can use the below 4 methods to read/write/del the data within
the same context.
```go
func GetContextData(ctx context.Context, tp, key any) any
func SetContextData(ctx context.Context, tp, key, value any)
func RemoveContextData(ctx context.Context, tp, key any)
func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error)
```
Then let's take a look at how `system.GetString` implement it.
```go
func GetSetting(ctx context.Context, key string) (string, error) {
return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
res, err := GetSettingNoCache(ctx, key)
if err != nil {
return "", err
}
return res.SettingValue, nil
})
})
}
```
First, it will check if context data include the setting object with the
key. If not, it will query from the global cache which may be memory or
a Redis cache. If not, it will get the object from the database. In the
end, if the object gets from the global cache or database, it will be
set into the context cache.
An object stored in the context cache will only be destroyed after the
context disappeared.
2023-02-15 06:37:34 -07:00
|
|
|
func Avatar(ctx context.Context, item interface{}, others ...interface{}) template.HTML {
|
2022-11-23 14:57:37 -07:00
|
|
|
size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
|
2020-12-03 11:46:11 -07:00
|
|
|
|
2021-11-23 20:51:08 -07:00
|
|
|
switch t := item.(type) {
|
2021-11-24 02:49:20 -07:00
|
|
|
case *user_model.User:
|
Add context cache as a request level cache (#22294)
To avoid duplicated load of the same data in an HTTP request, we can set
a context cache to do that. i.e. Some pages may load a user from a
database with the same id in different areas on the same page. But the
code is hidden in two different deep logic. How should we share the
user? As a result of this PR, now if both entry functions accept
`context.Context` as the first parameter and we just need to refactor
`GetUserByID` to reuse the user from the context cache. Then it will not
be loaded twice on an HTTP request.
But of course, sometimes we would like to reload an object from the
database, that's why `RemoveContextData` is also exposed.
The core context cache is here. It defines a new context
```go
type cacheContext struct {
ctx context.Context
data map[any]map[any]any
lock sync.RWMutex
}
var cacheContextKey = struct{}{}
func WithCacheContext(ctx context.Context) context.Context {
return context.WithValue(ctx, cacheContextKey, &cacheContext{
ctx: ctx,
data: make(map[any]map[any]any),
})
}
```
Then you can use the below 4 methods to read/write/del the data within
the same context.
```go
func GetContextData(ctx context.Context, tp, key any) any
func SetContextData(ctx context.Context, tp, key, value any)
func RemoveContextData(ctx context.Context, tp, key any)
func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error)
```
Then let's take a look at how `system.GetString` implement it.
```go
func GetSetting(ctx context.Context, key string) (string, error) {
return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
res, err := GetSettingNoCache(ctx, key)
if err != nil {
return "", err
}
return res.SettingValue, nil
})
})
}
```
First, it will check if context data include the setting object with the
key. If not, it will query from the global cache which may be memory or
a Redis cache. If not, it will get the object from the database. In the
end, if the object gets from the global cache or database, it will be
set into the context cache.
An object stored in the context cache will only be destroyed after the
context disappeared.
2023-02-15 06:37:34 -07:00
|
|
|
src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
|
2020-12-09 22:44:13 -07:00
|
|
|
if src != "" {
|
2021-11-23 20:51:08 -07:00
|
|
|
return AvatarHTML(src, size, class, t.DisplayName())
|
2020-12-09 22:44:13 -07:00
|
|
|
}
|
2022-05-11 04:09:36 -06:00
|
|
|
case *repo_model.Collaborator:
|
Add context cache as a request level cache (#22294)
To avoid duplicated load of the same data in an HTTP request, we can set
a context cache to do that. i.e. Some pages may load a user from a
database with the same id in different areas on the same page. But the
code is hidden in two different deep logic. How should we share the
user? As a result of this PR, now if both entry functions accept
`context.Context` as the first parameter and we just need to refactor
`GetUserByID` to reuse the user from the context cache. Then it will not
be loaded twice on an HTTP request.
But of course, sometimes we would like to reload an object from the
database, that's why `RemoveContextData` is also exposed.
The core context cache is here. It defines a new context
```go
type cacheContext struct {
ctx context.Context
data map[any]map[any]any
lock sync.RWMutex
}
var cacheContextKey = struct{}{}
func WithCacheContext(ctx context.Context) context.Context {
return context.WithValue(ctx, cacheContextKey, &cacheContext{
ctx: ctx,
data: make(map[any]map[any]any),
})
}
```
Then you can use the below 4 methods to read/write/del the data within
the same context.
```go
func GetContextData(ctx context.Context, tp, key any) any
func SetContextData(ctx context.Context, tp, key, value any)
func RemoveContextData(ctx context.Context, tp, key any)
func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error)
```
Then let's take a look at how `system.GetString` implement it.
```go
func GetSetting(ctx context.Context, key string) (string, error) {
return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
res, err := GetSettingNoCache(ctx, key)
if err != nil {
return "", err
}
return res.SettingValue, nil
})
})
}
```
First, it will check if context data include the setting object with the
key. If not, it will query from the global cache which may be memory or
a Redis cache. If not, it will get the object from the database. In the
end, if the object gets from the global cache or database, it will be
set into the context cache.
An object stored in the context cache will only be destroyed after the
context disappeared.
2023-02-15 06:37:34 -07:00
|
|
|
src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
|
2021-11-23 20:51:08 -07:00
|
|
|
if src != "" {
|
|
|
|
return AvatarHTML(src, size, class, t.DisplayName())
|
|
|
|
}
|
2022-03-29 00:29:02 -06:00
|
|
|
case *organization.Organization:
|
Add context cache as a request level cache (#22294)
To avoid duplicated load of the same data in an HTTP request, we can set
a context cache to do that. i.e. Some pages may load a user from a
database with the same id in different areas on the same page. But the
code is hidden in two different deep logic. How should we share the
user? As a result of this PR, now if both entry functions accept
`context.Context` as the first parameter and we just need to refactor
`GetUserByID` to reuse the user from the context cache. Then it will not
be loaded twice on an HTTP request.
But of course, sometimes we would like to reload an object from the
database, that's why `RemoveContextData` is also exposed.
The core context cache is here. It defines a new context
```go
type cacheContext struct {
ctx context.Context
data map[any]map[any]any
lock sync.RWMutex
}
var cacheContextKey = struct{}{}
func WithCacheContext(ctx context.Context) context.Context {
return context.WithValue(ctx, cacheContextKey, &cacheContext{
ctx: ctx,
data: make(map[any]map[any]any),
})
}
```
Then you can use the below 4 methods to read/write/del the data within
the same context.
```go
func GetContextData(ctx context.Context, tp, key any) any
func SetContextData(ctx context.Context, tp, key, value any)
func RemoveContextData(ctx context.Context, tp, key any)
func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error)
```
Then let's take a look at how `system.GetString` implement it.
```go
func GetSetting(ctx context.Context, key string) (string, error) {
return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
res, err := GetSettingNoCache(ctx, key)
if err != nil {
return "", err
}
return res.SettingValue, nil
})
})
}
```
First, it will check if context data include the setting object with the
key. If not, it will query from the global cache which may be memory or
a Redis cache. If not, it will get the object from the database. In the
end, if the object gets from the global cache or database, it will be
set into the context cache.
An object stored in the context cache will only be destroyed after the
context disappeared.
2023-02-15 06:37:34 -07:00
|
|
|
src := t.AsUser().AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
|
2020-12-09 22:44:13 -07:00
|
|
|
if src != "" {
|
2021-11-23 20:51:08 -07:00
|
|
|
return AvatarHTML(src, size, class, t.AsUser().DisplayName())
|
2020-12-09 22:44:13 -07:00
|
|
|
}
|
2020-12-03 11:46:11 -07:00
|
|
|
}
|
2021-11-23 20:51:08 -07:00
|
|
|
|
2020-12-07 21:14:28 -07:00
|
|
|
return template.HTML("")
|
|
|
|
}
|
|
|
|
|
2020-12-08 22:11:15 -07:00
|
|
|
// AvatarByAction renders user avatars from action. args: action, size (int), class (string)
|
Add context cache as a request level cache (#22294)
To avoid duplicated load of the same data in an HTTP request, we can set
a context cache to do that. i.e. Some pages may load a user from a
database with the same id in different areas on the same page. But the
code is hidden in two different deep logic. How should we share the
user? As a result of this PR, now if both entry functions accept
`context.Context` as the first parameter and we just need to refactor
`GetUserByID` to reuse the user from the context cache. Then it will not
be loaded twice on an HTTP request.
But of course, sometimes we would like to reload an object from the
database, that's why `RemoveContextData` is also exposed.
The core context cache is here. It defines a new context
```go
type cacheContext struct {
ctx context.Context
data map[any]map[any]any
lock sync.RWMutex
}
var cacheContextKey = struct{}{}
func WithCacheContext(ctx context.Context) context.Context {
return context.WithValue(ctx, cacheContextKey, &cacheContext{
ctx: ctx,
data: make(map[any]map[any]any),
})
}
```
Then you can use the below 4 methods to read/write/del the data within
the same context.
```go
func GetContextData(ctx context.Context, tp, key any) any
func SetContextData(ctx context.Context, tp, key, value any)
func RemoveContextData(ctx context.Context, tp, key any)
func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error)
```
Then let's take a look at how `system.GetString` implement it.
```go
func GetSetting(ctx context.Context, key string) (string, error) {
return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
res, err := GetSettingNoCache(ctx, key)
if err != nil {
return "", err
}
return res.SettingValue, nil
})
})
}
```
First, it will check if context data include the setting object with the
key. If not, it will query from the global cache which may be memory or
a Redis cache. If not, it will get the object from the database. In the
end, if the object gets from the global cache or database, it will be
set into the context cache.
An object stored in the context cache will only be destroyed after the
context disappeared.
2023-02-15 06:37:34 -07:00
|
|
|
func AvatarByAction(ctx context.Context, action *activities_model.Action, others ...interface{}) template.HTML {
|
|
|
|
action.LoadActUser(ctx)
|
|
|
|
return Avatar(ctx, action.ActUser, others...)
|
2020-12-08 22:11:15 -07:00
|
|
|
}
|
|
|
|
|
2020-12-07 21:14:28 -07:00
|
|
|
// RepoAvatar renders repo avatars. args: repo, size(int), class (string)
|
2021-12-09 18:27:50 -07:00
|
|
|
func RepoAvatar(repo *repo_model.Repository, others ...interface{}) template.HTML {
|
2022-11-23 14:57:37 -07:00
|
|
|
size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
|
2020-12-03 11:46:11 -07:00
|
|
|
|
2020-12-07 21:14:28 -07:00
|
|
|
src := repo.RelAvatarLink()
|
|
|
|
if src != "" {
|
2020-12-08 17:12:15 -07:00
|
|
|
return AvatarHTML(src, size, class, repo.FullName())
|
2020-12-07 21:14:28 -07:00
|
|
|
}
|
2020-12-03 11:46:11 -07:00
|
|
|
return template.HTML("")
|
|
|
|
}
|
|
|
|
|
|
|
|
// AvatarByEmail renders avatars by email address. args: email, name, size (int), class (string)
|
Add context cache as a request level cache (#22294)
To avoid duplicated load of the same data in an HTTP request, we can set
a context cache to do that. i.e. Some pages may load a user from a
database with the same id in different areas on the same page. But the
code is hidden in two different deep logic. How should we share the
user? As a result of this PR, now if both entry functions accept
`context.Context` as the first parameter and we just need to refactor
`GetUserByID` to reuse the user from the context cache. Then it will not
be loaded twice on an HTTP request.
But of course, sometimes we would like to reload an object from the
database, that's why `RemoveContextData` is also exposed.
The core context cache is here. It defines a new context
```go
type cacheContext struct {
ctx context.Context
data map[any]map[any]any
lock sync.RWMutex
}
var cacheContextKey = struct{}{}
func WithCacheContext(ctx context.Context) context.Context {
return context.WithValue(ctx, cacheContextKey, &cacheContext{
ctx: ctx,
data: make(map[any]map[any]any),
})
}
```
Then you can use the below 4 methods to read/write/del the data within
the same context.
```go
func GetContextData(ctx context.Context, tp, key any) any
func SetContextData(ctx context.Context, tp, key, value any)
func RemoveContextData(ctx context.Context, tp, key any)
func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error)
```
Then let's take a look at how `system.GetString` implement it.
```go
func GetSetting(ctx context.Context, key string) (string, error) {
return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
res, err := GetSettingNoCache(ctx, key)
if err != nil {
return "", err
}
return res.SettingValue, nil
})
})
}
```
First, it will check if context data include the setting object with the
key. If not, it will query from the global cache which may be memory or
a Redis cache. If not, it will get the object from the database. In the
end, if the object gets from the global cache or database, it will be
set into the context cache.
An object stored in the context cache will only be destroyed after the
context disappeared.
2023-02-15 06:37:34 -07:00
|
|
|
func AvatarByEmail(ctx context.Context, email, name string, others ...interface{}) template.HTML {
|
2022-11-23 14:57:37 -07:00
|
|
|
size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
|
Add context cache as a request level cache (#22294)
To avoid duplicated load of the same data in an HTTP request, we can set
a context cache to do that. i.e. Some pages may load a user from a
database with the same id in different areas on the same page. But the
code is hidden in two different deep logic. How should we share the
user? As a result of this PR, now if both entry functions accept
`context.Context` as the first parameter and we just need to refactor
`GetUserByID` to reuse the user from the context cache. Then it will not
be loaded twice on an HTTP request.
But of course, sometimes we would like to reload an object from the
database, that's why `RemoveContextData` is also exposed.
The core context cache is here. It defines a new context
```go
type cacheContext struct {
ctx context.Context
data map[any]map[any]any
lock sync.RWMutex
}
var cacheContextKey = struct{}{}
func WithCacheContext(ctx context.Context) context.Context {
return context.WithValue(ctx, cacheContextKey, &cacheContext{
ctx: ctx,
data: make(map[any]map[any]any),
})
}
```
Then you can use the below 4 methods to read/write/del the data within
the same context.
```go
func GetContextData(ctx context.Context, tp, key any) any
func SetContextData(ctx context.Context, tp, key, value any)
func RemoveContextData(ctx context.Context, tp, key any)
func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error)
```
Then let's take a look at how `system.GetString` implement it.
```go
func GetSetting(ctx context.Context, key string) (string, error) {
return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
res, err := GetSettingNoCache(ctx, key)
if err != nil {
return "", err
}
return res.SettingValue, nil
})
})
}
```
First, it will check if context data include the setting object with the
key. If not, it will query from the global cache which may be memory or
a Redis cache. If not, it will get the object from the database. In the
end, if the object gets from the global cache or database, it will be
set into the context cache.
An object stored in the context cache will only be destroyed after the
context disappeared.
2023-02-15 06:37:34 -07:00
|
|
|
src := avatars.GenerateEmailAvatarFastLink(ctx, email, size*setting.Avatar.RenderedSizeFactor)
|
2020-12-03 11:46:11 -07:00
|
|
|
|
|
|
|
if src != "" {
|
2020-12-08 17:12:15 -07:00
|
|
|
return AvatarHTML(src, size, class, name)
|
2020-12-03 11:46:11 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return template.HTML("")
|
|
|
|
}
|
|
|
|
|
2016-11-24 23:23:48 -07:00
|
|
|
// Safe render raw as HTML
|
2015-08-08 03:10:34 -06:00
|
|
|
func Safe(raw string) template.HTML {
|
|
|
|
return template.HTML(raw)
|
|
|
|
}
|
|
|
|
|
2016-11-24 23:23:48 -07:00
|
|
|
// Str2html render Markdown text to HTML
|
2014-04-10 12:20:58 -06:00
|
|
|
func Str2html(raw string) template.HTML {
|
2017-09-16 11:17:57 -06:00
|
|
|
return template.HTML(markup.Sanitize(raw))
|
2014-04-10 12:20:58 -06:00
|
|
|
}
|
|
|
|
|
2022-03-23 06:34:20 -06:00
|
|
|
// DotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent autolinkers from detecting these as urls
|
|
|
|
func DotEscape(raw string) string {
|
|
|
|
return strings.ReplaceAll(raw, ".", "\u200d.\u200d")
|
|
|
|
}
|
|
|
|
|
2015-01-30 16:05:20 -07:00
|
|
|
// RenderCommitMessage renders commit message with XSS-safe and special links.
|
2022-01-19 16:26:57 -07:00
|
|
|
func RenderCommitMessage(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
|
|
|
|
return RenderCommitMessageLink(ctx, msg, urlPrefix, "", metas)
|
2017-11-12 18:35:55 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// RenderCommitMessageLink renders commit message as a XXS-safe link to the provided
|
|
|
|
// default url, handling for special links.
|
2022-01-19 16:26:57 -07:00
|
|
|
func RenderCommitMessageLink(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
|
2015-09-18 19:57:06 -06:00
|
|
|
cleanMsg := template.HTMLEscapeString(msg)
|
2018-02-27 00:09:18 -07:00
|
|
|
// we can safely assume that it will not return any error, since there
|
|
|
|
// shouldn't be any special HTML.
|
2021-04-19 16:25:08 -06:00
|
|
|
fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
|
2022-01-19 16:26:57 -07:00
|
|
|
Ctx: ctx,
|
2021-04-19 16:25:08 -06:00
|
|
|
URLPrefix: urlPrefix,
|
|
|
|
DefaultLink: urlDefault,
|
|
|
|
Metas: metas,
|
|
|
|
}, cleanMsg)
|
2018-02-27 00:09:18 -07:00
|
|
|
if err != nil {
|
2019-04-02 01:48:31 -06:00
|
|
|
log.Error("RenderCommitMessage: %v", err)
|
2018-02-27 00:09:18 -07:00
|
|
|
return ""
|
|
|
|
}
|
2022-06-20 04:02:49 -06:00
|
|
|
msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
|
2017-11-12 18:35:55 -07:00
|
|
|
if len(msgLines) == 0 {
|
2015-12-06 16:18:12 -07:00
|
|
|
return template.HTML("")
|
2015-09-18 19:57:06 -06:00
|
|
|
}
|
2017-11-12 18:35:55 -07:00
|
|
|
return template.HTML(msgLines[0])
|
2015-01-30 16:05:20 -07:00
|
|
|
}
|
|
|
|
|
2019-09-10 03:03:30 -06:00
|
|
|
// RenderCommitMessageLinkSubject renders commit message as a XXS-safe link to
|
|
|
|
// the provided default url, handling for special links without email to links.
|
2022-01-19 16:26:57 -07:00
|
|
|
func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
|
2019-10-31 22:48:30 -06:00
|
|
|
msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace)
|
|
|
|
lineEnd := strings.IndexByte(msgLine, '\n')
|
|
|
|
if lineEnd > 0 {
|
|
|
|
msgLine = msgLine[:lineEnd]
|
|
|
|
}
|
|
|
|
msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace)
|
|
|
|
if len(msgLine) == 0 {
|
|
|
|
return template.HTML("")
|
|
|
|
}
|
|
|
|
|
2019-09-10 03:03:30 -06:00
|
|
|
// we can safely assume that it will not return any error, since there
|
|
|
|
// shouldn't be any special HTML.
|
2021-04-19 16:25:08 -06:00
|
|
|
renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
|
2022-01-19 16:26:57 -07:00
|
|
|
Ctx: ctx,
|
2021-04-19 16:25:08 -06:00
|
|
|
URLPrefix: urlPrefix,
|
|
|
|
DefaultLink: urlDefault,
|
|
|
|
Metas: metas,
|
|
|
|
}, template.HTMLEscapeString(msgLine))
|
2019-09-10 03:03:30 -06:00
|
|
|
if err != nil {
|
|
|
|
log.Error("RenderCommitMessageSubject: %v", err)
|
|
|
|
return template.HTML("")
|
|
|
|
}
|
2019-10-31 22:48:30 -06:00
|
|
|
return template.HTML(renderedMessage)
|
2019-09-10 03:03:30 -06:00
|
|
|
}
|
|
|
|
|
2017-11-29 22:08:40 -07:00
|
|
|
// RenderCommitBody extracts the body of a commit message without its title.
|
2022-01-19 16:26:57 -07:00
|
|
|
func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
|
2019-10-31 22:48:30 -06:00
|
|
|
msgLine := strings.TrimRightFunc(msg, unicode.IsSpace)
|
|
|
|
lineEnd := strings.IndexByte(msgLine, '\n')
|
|
|
|
if lineEnd > 0 {
|
|
|
|
msgLine = msgLine[lineEnd+1:]
|
|
|
|
} else {
|
|
|
|
return template.HTML("")
|
|
|
|
}
|
|
|
|
msgLine = strings.TrimLeftFunc(msgLine, unicode.IsSpace)
|
|
|
|
if len(msgLine) == 0 {
|
|
|
|
return template.HTML("")
|
|
|
|
}
|
|
|
|
|
2021-04-19 16:25:08 -06:00
|
|
|
renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
|
2022-01-19 16:26:57 -07:00
|
|
|
Ctx: ctx,
|
2021-04-19 16:25:08 -06:00
|
|
|
URLPrefix: urlPrefix,
|
|
|
|
Metas: metas,
|
|
|
|
}, template.HTMLEscapeString(msgLine))
|
2018-02-27 00:09:18 -07:00
|
|
|
if err != nil {
|
2019-04-02 01:48:31 -06:00
|
|
|
log.Error("RenderCommitMessage: %v", err)
|
2018-02-27 00:09:18 -07:00
|
|
|
return ""
|
|
|
|
}
|
2019-10-31 22:48:30 -06:00
|
|
|
return template.HTML(renderedMessage)
|
2017-11-29 22:08:40 -07:00
|
|
|
}
|
|
|
|
|
2022-10-15 12:24:41 -06:00
|
|
|
// Match text that is between back ticks.
|
|
|
|
var codeMatcher = regexp.MustCompile("`([^`]+)`")
|
|
|
|
|
|
|
|
// RenderCodeBlock renders "`…`" as highlighted "<code>" block.
|
|
|
|
// Intended for issue and PR titles, these containers should have styles for "<code>" elements
|
|
|
|
func RenderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
|
|
|
|
htmlWithCodeTags := codeMatcher.ReplaceAllString(string(htmlEscapedTextToRender), "<code>$1</code>") // replace with HTML <code> tags
|
|
|
|
return template.HTML(htmlWithCodeTags)
|
|
|
|
}
|
|
|
|
|
2020-12-03 03:50:47 -07:00
|
|
|
// RenderIssueTitle renders issue/pull title with defined post processors
|
2022-01-19 16:26:57 -07:00
|
|
|
func RenderIssueTitle(ctx context.Context, text, urlPrefix string, metas map[string]string) template.HTML {
|
2021-04-19 16:25:08 -06:00
|
|
|
renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
|
2022-01-19 16:26:57 -07:00
|
|
|
Ctx: ctx,
|
2021-04-19 16:25:08 -06:00
|
|
|
URLPrefix: urlPrefix,
|
|
|
|
Metas: metas,
|
|
|
|
}, template.HTMLEscapeString(text))
|
2020-12-03 03:50:47 -07:00
|
|
|
if err != nil {
|
|
|
|
log.Error("RenderIssueTitle: %v", err)
|
|
|
|
return template.HTML("")
|
|
|
|
}
|
|
|
|
return template.HTML(renderedText)
|
|
|
|
}
|
|
|
|
|
Scoped labels (#22585)
Add a new "exclusive" option per label. This makes it so that when the
label is named `scope/name`, no other label with the same `scope/`
prefix can be set on an issue.
The scope is determined by the last occurence of `/`, so for example
`scope/alpha/name` and `scope/beta/name` are considered to be in
different scopes and can coexist.
Exclusive scopes are not enforced by any database rules, however they
are enforced when editing labels at the models level, automatically
removing any existing labels in the same scope when either attaching a
new label or replacing all labels.
In menus use a circle instead of checkbox to indicate they function as
radio buttons per scope. Issue filtering by label ensures that only a
single scoped label is selected at a time. Clicking with alt key can be
used to remove a scoped label, both when editing individual issues and
batch editing.
Label rendering refactor for consistency and code simplification:
* Labels now consistently have the same shape, emojis and tooltips
everywhere. This includes the label list and label assignment menus.
* In label list, show description below label same as label menus.
* Don't use exactly black/white text colors to look a bit nicer.
* Simplify text color computation. There is no point computing luminance
in linear color space, as this is a perceptual problem and sRGB is
closer to perceptually linear.
* Increase height of label assignment menus to show more labels. Showing
only 3-4 labels at a time leads to a lot of scrolling.
* Render all labels with a new RenderLabel template helper function.
Label creation and editing in multiline modal menu:
* Change label creation to open a modal menu like label editing.
* Change menu layout to place name, description and colors on separate
lines.
* Don't color cancel button red in label editing modal menu.
* Align text to the left in model menu for better readability and
consistent with settings layout elsewhere.
Custom exclusive scoped label rendering:
* Display scoped label prefix and suffix with slightly darker and
lighter background color respectively, and a slanted edge between them
similar to the `/` symbol.
* In menus exclusive labels are grouped with a divider line.
---------
Co-authored-by: Yarden Shoham <hrsi88@gmail.com>
Co-authored-by: Lauris BH <lauris@nix.lv>
2023-02-18 12:17:39 -07:00
|
|
|
// RenderLabel renders a label
|
2023-03-05 14:59:05 -07:00
|
|
|
func RenderLabel(ctx context.Context, label *issues_model.Label) string {
|
Scoped labels (#22585)
Add a new "exclusive" option per label. This makes it so that when the
label is named `scope/name`, no other label with the same `scope/`
prefix can be set on an issue.
The scope is determined by the last occurence of `/`, so for example
`scope/alpha/name` and `scope/beta/name` are considered to be in
different scopes and can coexist.
Exclusive scopes are not enforced by any database rules, however they
are enforced when editing labels at the models level, automatically
removing any existing labels in the same scope when either attaching a
new label or replacing all labels.
In menus use a circle instead of checkbox to indicate they function as
radio buttons per scope. Issue filtering by label ensures that only a
single scoped label is selected at a time. Clicking with alt key can be
used to remove a scoped label, both when editing individual issues and
batch editing.
Label rendering refactor for consistency and code simplification:
* Labels now consistently have the same shape, emojis and tooltips
everywhere. This includes the label list and label assignment menus.
* In label list, show description below label same as label menus.
* Don't use exactly black/white text colors to look a bit nicer.
* Simplify text color computation. There is no point computing luminance
in linear color space, as this is a perceptual problem and sRGB is
closer to perceptually linear.
* Increase height of label assignment menus to show more labels. Showing
only 3-4 labels at a time leads to a lot of scrolling.
* Render all labels with a new RenderLabel template helper function.
Label creation and editing in multiline modal menu:
* Change label creation to open a modal menu like label editing.
* Change menu layout to place name, description and colors on separate
lines.
* Don't color cancel button red in label editing modal menu.
* Align text to the left in model menu for better readability and
consistent with settings layout elsewhere.
Custom exclusive scoped label rendering:
* Display scoped label prefix and suffix with slightly darker and
lighter background color respectively, and a slanted edge between them
similar to the `/` symbol.
* In menus exclusive labels are grouped with a divider line.
---------
Co-authored-by: Yarden Shoham <hrsi88@gmail.com>
Co-authored-by: Lauris BH <lauris@nix.lv>
2023-02-18 12:17:39 -07:00
|
|
|
labelScope := label.ExclusiveScope()
|
|
|
|
|
|
|
|
textColor := "#111"
|
|
|
|
if label.UseLightTextColor() {
|
|
|
|
textColor = "#eee"
|
|
|
|
}
|
|
|
|
|
|
|
|
description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description))
|
|
|
|
|
|
|
|
if labelScope == "" {
|
|
|
|
// Regular label
|
|
|
|
return fmt.Sprintf("<div class='ui label' style='color: %s !important; background-color: %s !important' title='%s'>%s</div>",
|
2023-03-05 14:59:05 -07:00
|
|
|
textColor, label.Color, description, RenderEmoji(ctx, label.Name))
|
Scoped labels (#22585)
Add a new "exclusive" option per label. This makes it so that when the
label is named `scope/name`, no other label with the same `scope/`
prefix can be set on an issue.
The scope is determined by the last occurence of `/`, so for example
`scope/alpha/name` and `scope/beta/name` are considered to be in
different scopes and can coexist.
Exclusive scopes are not enforced by any database rules, however they
are enforced when editing labels at the models level, automatically
removing any existing labels in the same scope when either attaching a
new label or replacing all labels.
In menus use a circle instead of checkbox to indicate they function as
radio buttons per scope. Issue filtering by label ensures that only a
single scoped label is selected at a time. Clicking with alt key can be
used to remove a scoped label, both when editing individual issues and
batch editing.
Label rendering refactor for consistency and code simplification:
* Labels now consistently have the same shape, emojis and tooltips
everywhere. This includes the label list and label assignment menus.
* In label list, show description below label same as label menus.
* Don't use exactly black/white text colors to look a bit nicer.
* Simplify text color computation. There is no point computing luminance
in linear color space, as this is a perceptual problem and sRGB is
closer to perceptually linear.
* Increase height of label assignment menus to show more labels. Showing
only 3-4 labels at a time leads to a lot of scrolling.
* Render all labels with a new RenderLabel template helper function.
Label creation and editing in multiline modal menu:
* Change label creation to open a modal menu like label editing.
* Change menu layout to place name, description and colors on separate
lines.
* Don't color cancel button red in label editing modal menu.
* Align text to the left in model menu for better readability and
consistent with settings layout elsewhere.
Custom exclusive scoped label rendering:
* Display scoped label prefix and suffix with slightly darker and
lighter background color respectively, and a slanted edge between them
similar to the `/` symbol.
* In menus exclusive labels are grouped with a divider line.
---------
Co-authored-by: Yarden Shoham <hrsi88@gmail.com>
Co-authored-by: Lauris BH <lauris@nix.lv>
2023-02-18 12:17:39 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Scoped label
|
2023-03-05 14:59:05 -07:00
|
|
|
scopeText := RenderEmoji(ctx, labelScope)
|
|
|
|
itemText := RenderEmoji(ctx, label.Name[len(labelScope)+1:])
|
Scoped labels (#22585)
Add a new "exclusive" option per label. This makes it so that when the
label is named `scope/name`, no other label with the same `scope/`
prefix can be set on an issue.
The scope is determined by the last occurence of `/`, so for example
`scope/alpha/name` and `scope/beta/name` are considered to be in
different scopes and can coexist.
Exclusive scopes are not enforced by any database rules, however they
are enforced when editing labels at the models level, automatically
removing any existing labels in the same scope when either attaching a
new label or replacing all labels.
In menus use a circle instead of checkbox to indicate they function as
radio buttons per scope. Issue filtering by label ensures that only a
single scoped label is selected at a time. Clicking with alt key can be
used to remove a scoped label, both when editing individual issues and
batch editing.
Label rendering refactor for consistency and code simplification:
* Labels now consistently have the same shape, emojis and tooltips
everywhere. This includes the label list and label assignment menus.
* In label list, show description below label same as label menus.
* Don't use exactly black/white text colors to look a bit nicer.
* Simplify text color computation. There is no point computing luminance
in linear color space, as this is a perceptual problem and sRGB is
closer to perceptually linear.
* Increase height of label assignment menus to show more labels. Showing
only 3-4 labels at a time leads to a lot of scrolling.
* Render all labels with a new RenderLabel template helper function.
Label creation and editing in multiline modal menu:
* Change label creation to open a modal menu like label editing.
* Change menu layout to place name, description and colors on separate
lines.
* Don't color cancel button red in label editing modal menu.
* Align text to the left in model menu for better readability and
consistent with settings layout elsewhere.
Custom exclusive scoped label rendering:
* Display scoped label prefix and suffix with slightly darker and
lighter background color respectively, and a slanted edge between them
similar to the `/` symbol.
* In menus exclusive labels are grouped with a divider line.
---------
Co-authored-by: Yarden Shoham <hrsi88@gmail.com>
Co-authored-by: Lauris BH <lauris@nix.lv>
2023-02-18 12:17:39 -07:00
|
|
|
|
|
|
|
itemColor := label.Color
|
|
|
|
scopeColor := label.Color
|
|
|
|
if r, g, b, err := label.ColorRGB(); err == nil {
|
|
|
|
// Make scope and item background colors slightly darker and lighter respectively.
|
|
|
|
// More contrast needed with higher luminance, empirically tweaked.
|
|
|
|
luminance := (0.299*r + 0.587*g + 0.114*b) / 255
|
2023-03-12 18:46:13 -06:00
|
|
|
contrast := 0.01 + luminance*0.03
|
Scoped labels (#22585)
Add a new "exclusive" option per label. This makes it so that when the
label is named `scope/name`, no other label with the same `scope/`
prefix can be set on an issue.
The scope is determined by the last occurence of `/`, so for example
`scope/alpha/name` and `scope/beta/name` are considered to be in
different scopes and can coexist.
Exclusive scopes are not enforced by any database rules, however they
are enforced when editing labels at the models level, automatically
removing any existing labels in the same scope when either attaching a
new label or replacing all labels.
In menus use a circle instead of checkbox to indicate they function as
radio buttons per scope. Issue filtering by label ensures that only a
single scoped label is selected at a time. Clicking with alt key can be
used to remove a scoped label, both when editing individual issues and
batch editing.
Label rendering refactor for consistency and code simplification:
* Labels now consistently have the same shape, emojis and tooltips
everywhere. This includes the label list and label assignment menus.
* In label list, show description below label same as label menus.
* Don't use exactly black/white text colors to look a bit nicer.
* Simplify text color computation. There is no point computing luminance
in linear color space, as this is a perceptual problem and sRGB is
closer to perceptually linear.
* Increase height of label assignment menus to show more labels. Showing
only 3-4 labels at a time leads to a lot of scrolling.
* Render all labels with a new RenderLabel template helper function.
Label creation and editing in multiline modal menu:
* Change label creation to open a modal menu like label editing.
* Change menu layout to place name, description and colors on separate
lines.
* Don't color cancel button red in label editing modal menu.
* Align text to the left in model menu for better readability and
consistent with settings layout elsewhere.
Custom exclusive scoped label rendering:
* Display scoped label prefix and suffix with slightly darker and
lighter background color respectively, and a slanted edge between them
similar to the `/` symbol.
* In menus exclusive labels are grouped with a divider line.
---------
Co-authored-by: Yarden Shoham <hrsi88@gmail.com>
Co-authored-by: Lauris BH <lauris@nix.lv>
2023-02-18 12:17:39 -07:00
|
|
|
// Ensure we add the same amount of contrast also near 0 and 1.
|
|
|
|
darken := contrast + math.Max(luminance+contrast-1.0, 0.0)
|
|
|
|
lighten := contrast + math.Max(contrast-luminance, 0.0)
|
|
|
|
// Compute factor to keep RGB values proportional.
|
|
|
|
darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0)
|
|
|
|
lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0)
|
|
|
|
|
|
|
|
scopeBytes := []byte{
|
|
|
|
uint8(math.Min(math.Round(r*darkenFactor), 255)),
|
|
|
|
uint8(math.Min(math.Round(g*darkenFactor), 255)),
|
|
|
|
uint8(math.Min(math.Round(b*darkenFactor), 255)),
|
|
|
|
}
|
|
|
|
itemBytes := []byte{
|
|
|
|
uint8(math.Min(math.Round(r*lightenFactor), 255)),
|
|
|
|
uint8(math.Min(math.Round(g*lightenFactor), 255)),
|
|
|
|
uint8(math.Min(math.Round(b*lightenFactor), 255)),
|
|
|
|
}
|
|
|
|
|
|
|
|
itemColor = "#" + hex.EncodeToString(itemBytes)
|
|
|
|
scopeColor = "#" + hex.EncodeToString(scopeBytes)
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Sprintf("<span class='ui label scope-parent' title='%s'>"+
|
|
|
|
"<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>"+
|
|
|
|
"<div class='ui label scope-right' style='color: %s !important; background-color: %s !important''>%s</div>"+
|
|
|
|
"</span>",
|
|
|
|
description,
|
|
|
|
textColor, scopeColor, scopeText,
|
|
|
|
textColor, itemColor, itemText)
|
|
|
|
}
|
|
|
|
|
2020-04-28 12:05:39 -06:00
|
|
|
// RenderEmoji renders html text with emoji post processors
|
2023-03-05 14:59:05 -07:00
|
|
|
func RenderEmoji(ctx context.Context, text string) template.HTML {
|
|
|
|
renderedText, err := markup.RenderEmoji(&markup.RenderContext{Ctx: ctx},
|
|
|
|
template.HTMLEscapeString(text))
|
2020-04-28 12:05:39 -06:00
|
|
|
if err != nil {
|
|
|
|
log.Error("RenderEmoji: %v", err)
|
|
|
|
return template.HTML("")
|
|
|
|
}
|
|
|
|
return template.HTML(renderedText)
|
|
|
|
}
|
|
|
|
|
2022-01-20 10:46:10 -07:00
|
|
|
// ReactionToEmoji renders emoji for use in reactions
|
2020-04-28 12:05:39 -06:00
|
|
|
func ReactionToEmoji(reaction string) template.HTML {
|
|
|
|
val := emoji.FromCode(reaction)
|
|
|
|
if val != nil {
|
|
|
|
return template.HTML(val.Emoji)
|
|
|
|
}
|
|
|
|
val = emoji.FromAlias(reaction)
|
|
|
|
if val != nil {
|
|
|
|
return template.HTML(val.Emoji)
|
|
|
|
}
|
2021-11-16 11:18:25 -07:00
|
|
|
return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction)))
|
2020-04-28 12:05:39 -06:00
|
|
|
}
|
|
|
|
|
2019-05-24 01:52:05 -06:00
|
|
|
// RenderNote renders the contents of a git-notes file as a commit message.
|
2022-01-19 16:26:57 -07:00
|
|
|
func RenderNote(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
|
2019-05-24 01:52:05 -06:00
|
|
|
cleanMsg := template.HTMLEscapeString(msg)
|
2021-04-19 16:25:08 -06:00
|
|
|
fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
|
2022-01-19 16:26:57 -07:00
|
|
|
Ctx: ctx,
|
2021-04-19 16:25:08 -06:00
|
|
|
URLPrefix: urlPrefix,
|
|
|
|
Metas: metas,
|
|
|
|
}, cleanMsg)
|
2019-05-24 01:52:05 -06:00
|
|
|
if err != nil {
|
|
|
|
log.Error("RenderNote: %v", err)
|
|
|
|
return ""
|
|
|
|
}
|
2022-06-20 04:02:49 -06:00
|
|
|
return template.HTML(fullMessage)
|
2019-05-24 01:52:05 -06:00
|
|
|
}
|
|
|
|
|
2017-11-29 22:08:40 -07:00
|
|
|
// IsMultilineCommitMessage checks to see if a commit message contains multiple lines.
|
|
|
|
func IsMultilineCommitMessage(msg string) bool {
|
2018-06-15 08:07:48 -06:00
|
|
|
return strings.Count(strings.TrimSpace(msg), "\n") >= 1
|
2017-11-29 22:08:40 -07:00
|
|
|
}
|
|
|
|
|
2016-11-24 23:23:48 -07:00
|
|
|
// Actioner describes an action
|
2014-04-10 12:20:58 -06:00
|
|
|
type Actioner interface {
|
2022-08-24 20:31:57 -06:00
|
|
|
GetOpType() activities_model.ActionType
|
2014-04-10 12:20:58 -06:00
|
|
|
GetActUserName() string
|
2014-05-09 00:42:50 -06:00
|
|
|
GetRepoUserName() string
|
2014-04-10 12:20:58 -06:00
|
|
|
GetRepoName() string
|
2015-09-01 07:29:52 -06:00
|
|
|
GetRepoPath() string
|
|
|
|
GetRepoLink() string
|
2014-04-10 12:20:58 -06:00
|
|
|
GetBranch() string
|
|
|
|
GetContent() string
|
2015-09-01 07:29:52 -06:00
|
|
|
GetCreate() time.Time
|
|
|
|
GetIssueInfos() []string
|
2014-04-10 12:20:58 -06:00
|
|
|
}
|
|
|
|
|
2017-09-19 19:22:42 -06:00
|
|
|
// ActionIcon accepts an action operation type and returns an icon class name.
|
2022-08-24 20:31:57 -06:00
|
|
|
func ActionIcon(opType activities_model.ActionType) string {
|
2014-04-10 12:20:58 -06:00
|
|
|
switch opType {
|
2022-08-24 20:31:57 -06:00
|
|
|
case activities_model.ActionCreateRepo, activities_model.ActionTransferRepo, activities_model.ActionRenameRepo:
|
2014-07-25 22:24:27 -06:00
|
|
|
return "repo"
|
2022-08-24 20:31:57 -06:00
|
|
|
case activities_model.ActionCommitRepo, activities_model.ActionPushTag, activities_model.ActionDeleteTag, activities_model.ActionDeleteBranch:
|
2014-07-25 22:24:27 -06:00
|
|
|
return "git-commit"
|
2022-08-24 20:31:57 -06:00
|
|
|
case activities_model.ActionCreateIssue:
|
2014-07-25 22:24:27 -06:00
|
|
|
return "issue-opened"
|
2022-08-24 20:31:57 -06:00
|
|
|
case activities_model.ActionCreatePullRequest:
|
2015-11-16 09:39:48 -07:00
|
|
|
return "git-pull-request"
|
2022-08-24 20:31:57 -06:00
|
|
|
case activities_model.ActionCommentIssue, activities_model.ActionCommentPull:
|
2016-07-15 22:45:13 -06:00
|
|
|
return "comment-discussion"
|
2022-11-03 09:49:00 -06:00
|
|
|
case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest:
|
2015-11-16 09:39:48 -07:00
|
|
|
return "git-merge"
|
2022-08-24 20:31:57 -06:00
|
|
|
case activities_model.ActionCloseIssue, activities_model.ActionClosePullRequest:
|
2016-02-22 10:40:00 -07:00
|
|
|
return "issue-closed"
|
2022-08-24 20:31:57 -06:00
|
|
|
case activities_model.ActionReopenIssue, activities_model.ActionReopenPullRequest:
|
2016-03-05 10:58:51 -07:00
|
|
|
return "issue-reopened"
|
2022-08-24 20:31:57 -06:00
|
|
|
case activities_model.ActionMirrorSyncPush, activities_model.ActionMirrorSyncCreate, activities_model.ActionMirrorSyncDelete:
|
2020-12-10 16:06:45 -07:00
|
|
|
return "mirror"
|
2022-08-24 20:31:57 -06:00
|
|
|
case activities_model.ActionApprovePullRequest:
|
2020-04-23 22:58:14 -06:00
|
|
|
return "check"
|
2022-08-24 20:31:57 -06:00
|
|
|
case activities_model.ActionRejectPullRequest:
|
2020-07-17 09:15:12 -06:00
|
|
|
return "diff"
|
2022-08-24 20:31:57 -06:00
|
|
|
case activities_model.ActionPublishRelease:
|
2020-07-29 13:20:54 -06:00
|
|
|
return "tag"
|
2022-08-24 20:31:57 -06:00
|
|
|
case activities_model.ActionPullReviewDismissed:
|
2021-02-11 10:32:25 -07:00
|
|
|
return "x"
|
2014-04-10 12:20:58 -06:00
|
|
|
default:
|
2020-04-23 22:58:14 -06:00
|
|
|
return "question"
|
2014-04-10 12:20:58 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-24 23:23:48 -07:00
|
|
|
// ActionContent2Commits converts action content to push commits
|
2020-01-10 02:34:21 -07:00
|
|
|
func ActionContent2Commits(act Actioner) *repository.PushCommits {
|
|
|
|
push := repository.NewPushCommits()
|
2021-03-01 14:08:10 -07:00
|
|
|
|
2021-03-05 21:09:49 -07:00
|
|
|
if act == nil || act.GetContent() == "" {
|
|
|
|
return push
|
|
|
|
}
|
|
|
|
|
2015-11-13 15:10:25 -07:00
|
|
|
if err := json.Unmarshal([]byte(act.GetContent()), push); err != nil {
|
2019-04-02 01:48:31 -06:00
|
|
|
log.Error("json.Unmarshal:\n%s\nERROR: %v", act.GetContent(), err)
|
2014-07-25 22:24:27 -06:00
|
|
|
}
|
2021-08-25 17:04:58 -06:00
|
|
|
|
|
|
|
if push.Len == 0 {
|
|
|
|
push.Len = len(push.Commits)
|
|
|
|
}
|
|
|
|
|
2014-07-25 22:24:27 -06:00
|
|
|
return push
|
|
|
|
}
|
|
|
|
|
2016-11-24 23:23:48 -07:00
|
|
|
// DiffLineTypeToStr returns diff line type name
|
2014-04-10 12:20:58 -06:00
|
|
|
func DiffLineTypeToStr(diffType int) string {
|
|
|
|
switch diffType {
|
|
|
|
case 2:
|
|
|
|
return "add"
|
|
|
|
case 3:
|
|
|
|
return "del"
|
|
|
|
case 4:
|
|
|
|
return "tag"
|
|
|
|
}
|
|
|
|
return "same"
|
|
|
|
}
|
2017-10-14 17:17:39 -06:00
|
|
|
|
2021-09-18 10:22:51 -06:00
|
|
|
// MigrationIcon returns a SVG name matching the service an issue/comment was migrated from
|
2019-07-07 20:14:12 -06:00
|
|
|
func MigrationIcon(hostname string) string {
|
|
|
|
switch hostname {
|
|
|
|
case "github.com":
|
2021-09-18 10:22:51 -06:00
|
|
|
return "octicon-mark-github"
|
2019-07-07 20:14:12 -06:00
|
|
|
default:
|
2021-09-18 10:22:51 -06:00
|
|
|
return "gitea-git"
|
2019-07-07 20:14:12 -06:00
|
|
|
}
|
|
|
|
}
|
2019-11-07 06:34:28 -07:00
|
|
|
|
2021-06-14 11:20:43 -06:00
|
|
|
type remoteAddress struct {
|
|
|
|
Address string
|
|
|
|
Username string
|
|
|
|
Password string
|
|
|
|
}
|
|
|
|
|
2022-08-14 21:12:01 -06:00
|
|
|
func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string, ignoreOriginalURL bool) remoteAddress {
|
2021-06-14 11:20:43 -06:00
|
|
|
a := remoteAddress{}
|
2022-06-11 07:50:14 -06:00
|
|
|
|
|
|
|
remoteURL := m.OriginalURL
|
2022-08-14 21:12:01 -06:00
|
|
|
if ignoreOriginalURL || remoteURL == "" {
|
2022-06-11 07:50:14 -06:00
|
|
|
var err error
|
|
|
|
remoteURL, err = git.GetRemoteAddress(ctx, m.RepoPath(), remoteName)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("GetRemoteURL %v", err)
|
|
|
|
return a
|
|
|
|
}
|
|
|
|
}
|
2021-06-14 11:20:43 -06:00
|
|
|
|
2022-06-11 07:50:14 -06:00
|
|
|
u, err := giturl.Parse(remoteURL)
|
2021-06-14 11:20:43 -06:00
|
|
|
if err != nil {
|
2022-06-11 07:50:14 -06:00
|
|
|
log.Error("giturl.Parse %v", err)
|
2021-06-14 11:20:43 -06:00
|
|
|
return a
|
|
|
|
}
|
|
|
|
|
2022-06-11 07:50:14 -06:00
|
|
|
if u.Scheme != "ssh" && u.Scheme != "file" {
|
|
|
|
if u.User != nil {
|
|
|
|
a.Username = u.User.Username()
|
|
|
|
a.Password, _ = u.User.Password()
|
|
|
|
}
|
|
|
|
u.User = nil
|
2021-06-14 11:20:43 -06:00
|
|
|
}
|
|
|
|
a.Address = u.String()
|
|
|
|
|
|
|
|
return a
|
|
|
|
}
|
2022-06-12 06:08:23 -06:00
|
|
|
|
Use a general Eval function for expressions in templates. (#23927)
One of the proposals in #23328
This PR introduces a simple expression calculator
(templates/eval/eval.go), it can do basic expression calculations.
Many untested template helper functions like `Mul` `Add` can be replaced
by this new approach.
Then these `Add` / `Mul` / `percentage` / `Subtract` / `DiffStatsWidth`
could all use this `Eval`.
And it provides enhancements for Golang templates, and improves
readability.
Some examples:
----
* Before: `{{Add (Mul $glyph.Row 12) 12}}`
* After: `{{Eval $glyph.Row "*" 12 "+" 12}}`
----
* Before: `{{if lt (Add $i 1) (len $.Topics)}}`
* After: `{{if Eval $i "+" 1 "<" (len $.Topics)}}`
## FAQ
### Why not use an existing expression package?
We need a highly customized expression engine:
* do the calculation on the fly, without pre-compiling
* deal with int/int64/float64 types, to make the result could be used in
Golang template.
* make the syntax could be used in the Golang template directly
* do not introduce too much complex or strange syntax, we just need a
simple calculator.
* it needs to strictly follow Golang template's behavior, for example,
Golang template treats all non-zero values as truth, but many 3rd
packages don't do so.
### What's the benefit?
* Developers don't need to add more `Add`/`Mul`/`Sub`-like functions,
they were getting more and more.
Now, only one `Eval` is enough for all cases.
* The new code reads better than old `{{Add (Mul $glyph.Row 12) 12}}`,
the old one isn't familiar to most procedural programming developers
(eg, the Golang expression syntax).
* The `Eval` is fully covered by tests, many old `Add`/`Mul`-like
functions were never tested.
### The performance?
It doesn't use `reflect`, it doesn't need to parse or compile when used
in Golang template, the performance is as fast as native Go template.
### Is it too complex? Could it be unstable?
The expression calculator program is a common homework for computer
science students, and it's widely used as a teaching and practicing
purpose for developers. The algorithm is pretty well-known.
The behavior can be clearly defined, it is stable.
2023-04-07 07:25:49 -06:00
|
|
|
// Eval the expression and return the result, see the comment of eval.Expr for details.
|
|
|
|
// To use this helper function in templates, pass each token as a separate parameter.
|
|
|
|
//
|
|
|
|
// {{ $int64 := Eval $var "+" 1 }}
|
|
|
|
// {{ $float64 := Eval $var "+" 1.0 }}
|
|
|
|
//
|
|
|
|
// Golang's template supports comparable int types, so the int64 result can be used in later statements like {{if lt $int64 10}}
|
|
|
|
func Eval(tokens ...any) (any, error) {
|
|
|
|
n, err := eval.Expr(tokens...)
|
|
|
|
return n.Value, err
|
|
|
|
}
|