2018-09-29 11:33:54 +03:00
// Copyright 2018 The Gitea Authors. All rights reserved.
2014-04-10 14:20:58 -04:00
// Copyright 2014 The Gogs Authors. All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2014-04-10 14:20:58 -04:00
2016-12-06 18:58:31 +01:00
package templates
2014-04-10 14:20:58 -04:00
import (
"fmt"
2018-02-27 08:09:18 +01:00
"html"
2014-04-10 14:20:58 -04:00
"html/template"
2017-11-28 01:43:51 -08:00
"net/url"
2014-04-10 14:20:58 -04:00
"strings"
"time"
2014-05-25 20:11:25 -04:00
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/modules/base"
2020-04-28 14:05:39 -04:00
"code.gitea.io/gitea/modules/emoji"
2017-09-17 01:17:57 +08:00
"code.gitea.io/gitea/modules/markup"
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/modules/setting"
2020-07-12 11:10:56 +02: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 21:25:49 +08:00
"code.gitea.io/gitea/modules/templates/eval"
2019-08-15 22:46:21 +08:00
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
2019-09-06 10:20:09 +08:00
"code.gitea.io/gitea/services/gitdiff"
2014-04-10 14:20:58 -04:00
)
2016-11-25 14:23:48 +08:00
// NewFuncMap returns functions for injecting to templates
2023-04-30 20:22:23 +08:00
func NewFuncMap ( ) template . FuncMap {
2023-07-04 20:36:08 +02:00
return map [ string ] any {
2023-08-08 09:22:47 +08:00
"ctx" : func ( ) any { return nil } , // template context function
2023-04-29 20:02:29 +08:00
"DumpVar" : dumpVar ,
2023-04-08 21:15:22 +08: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 ,
2023-04-29 20:02:29 +08:00
"DotEscape" : DotEscape ,
2023-04-08 21:15:22 +08:00
"PathEscape" : url . PathEscape ,
"PathEscapeSegments" : util . PathEscapeSegments ,
2023-04-23 02:16:22 +08:00
// utils
"StringUtils" : NewStringUtils ,
"SliceUtils" : NewSliceUtils ,
2023-04-29 20:02:29 +08:00
"JsonUtils" : NewJsonUtils ,
2023-04-08 21:15:22 +08:00
// -----------------------------------------------------------------
// svg / avatar / icon
2023-08-10 11:19:39 +08:00
"svg" : svg . RenderHTML ,
"EntryIcon" : base . EntryIcon ,
"MigrationIcon" : MigrationIcon ,
"ActionIcon" : ActionIcon ,
2023-04-08 21:15:22 +08:00
2023-04-29 20:02:29 +08:00
"SortArrow" : SortArrow ,
2023-04-08 21:15:22 +08:00
// -----------------------------------------------------------------
// time / number / format
"FileSize" : base . FileSize ,
"CountFmt" : base . FormatNumberSI ,
"TimeSince" : timeutil . TimeSince ,
"TimeSinceUnix" : timeutil . TimeSinceUnix ,
2023-04-23 22:12:33 +03:00
"DateTime" : timeutil . DateTime ,
2023-04-08 21:15:22 +08:00
"Sec2Time" : util . SecToTime ,
"LoadTimes" : func ( startTime time . Time ) string {
return fmt . Sprint ( time . Since ( startTime ) . Nanoseconds ( ) / 1e6 ) + "ms"
} ,
// -----------------------------------------------------------------
// setting
2016-03-06 16:40:04 -05:00
"AppName" : func ( ) string {
return setting . AppName
} ,
"AppSubUrl" : func ( ) string {
2016-11-27 18:14:25 +08:00
return setting . AppSubURL
2016-03-06 16:40:04 -05:00
} ,
2021-05-08 16:27:25 +02:00
"AssetUrlPrefix" : func ( ) string {
2021-04-28 20:35:06 +08:00
return setting . StaticURLPrefix + "/assets"
2019-10-22 14:11:01 +02:00
} ,
2016-03-06 16:40:04 -05:00
"AppUrl" : func ( ) string {
2023-02-10 00:31:30 +08: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 18:14:25 +08:00
return setting . AppURL
2016-03-06 16:40:04 -05:00
} ,
"AppVer" : func ( ) string {
return setting . AppVer
} ,
2023-04-07 15:31:41 +08:00
"AppDomain" : func ( ) string { // documented in mail-templates.md
2016-03-06 16:40:04 -05:00
return setting . Domain
} ,
2022-08-23 14:58:04 +02:00
"AssetVersion" : func ( ) string {
return setting . AssetVersion
} ,
2019-05-08 10:41:35 +02:00
"DefaultShowFullName" : func ( ) bool {
return setting . UI . DefaultShowFullName
} ,
2016-09-01 07:01:32 +02:00
"ShowFooterTemplateLoadTime" : func ( ) bool {
2023-04-23 07:38:25 +08:00
return setting . Other . ShowFooterTemplateLoadTime
2016-09-01 07:01:32 +02:00
} ,
2019-12-28 00:43:56 +01:00
"AllowedReactions" : func ( ) [ ] string {
return setting . UI . Reactions
} ,
2021-06-29 16:28:38 +02:00
"CustomEmojis" : func ( ) map [ string ] string {
return setting . UI . CustomEmojisMap
} ,
2017-04-01 03:03:01 +02: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
} ,
2021-02-19 23:06:56 +00:00
"EnableTimetracking" : func ( ) bool {
return setting . Service . EnableTimetracking
} ,
2017-09-12 05:25:42 -04:00
"DisableGitHooks" : func ( ) bool {
return setting . DisableGitHooks
} ,
2021-02-11 18:34:34 +01:00
"DisableWebhooks" : func ( ) bool {
return setting . DisableWebhooks
} ,
2018-08-24 07:00:22 +02:00
"DisableImportLocal" : func ( ) bool {
return ! setting . ImportLocalPaths
} ,
2018-07-05 17:25:04 -04:00
"DefaultTheme" : func ( ) string {
return setting . UI . DefaultTheme
} ,
2023-07-04 20:36:08 +02:00
"NotificationSettings" : func ( ) map [ string ] any {
return map [ string ] any {
2020-05-07 22:49:00 +01: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-24 04:57:38 +01:00
}
} ,
2023-04-08 21:15:22 +08:00
"MermaidMaxSourceCharacters" : func ( ) int {
return setting . MermaidMaxSourceCharacters
} ,
2020-11-08 17:21:54 +00:00
2023-04-08 21:15:22 +08:00
// -----------------------------------------------------------------
// render
"RenderCommitMessage" : RenderCommitMessage ,
"RenderCommitMessageLinkSubject" : RenderCommitMessageLinkSubject ,
2020-11-08 17:21:54 +00:00
2023-04-08 21:15:22 +08:00
"RenderCommitBody" : RenderCommitBody ,
"RenderCodeBlock" : RenderCodeBlock ,
"RenderIssueTitle" : RenderIssueTitle ,
"RenderEmoji" : RenderEmoji ,
"RenderEmojiPlain" : emoji . ReplaceAliases ,
"ReactionToEmoji" : ReactionToEmoji ,
"RenderNote" : RenderNote ,
2020-06-25 00:23:05 +02:00
2023-04-29 20:02:29 +08:00
"RenderMarkdownToHtml" : RenderMarkdownToHtml ,
"RenderLabel" : RenderLabel ,
"RenderLabels" : RenderLabels ,
2023-04-08 21:15:22 +08:00
// -----------------------------------------------------------------
// misc
"ShortSha" : base . ShortSha ,
"ActionContent2Commits" : ActionContent2Commits ,
"IsMultilineCommitMessage" : IsMultilineCommitMessage ,
"CommentMustAsDiff" : gitdiff . CommentMustAsDiff ,
"MirrorRemoteAddress" : mirrorRemoteAddress ,
2023-04-29 20:02:29 +08:00
"FilenameIsImage" : FilenameIsImage ,
"TabSizeClass" : TabSizeClass ,
2023-04-30 20:22:23 +08:00
}
2019-11-07 10:34:28 -03:00
}
2016-11-25 14:23:48 +08:00
// Safe render raw as HTML
2015-08-08 17:10:34 +08:00
func Safe ( raw string ) template . HTML {
return template . HTML ( raw )
}
2016-11-25 14:23:48 +08:00
// Str2html render Markdown text to HTML
2014-04-10 14:20:58 -04:00
func Str2html ( raw string ) template . HTML {
2017-09-17 01:17:57 +08:00
return template . HTML ( markup . Sanitize ( raw ) )
2014-04-10 14:20:58 -04:00
}
2022-03-23 12:34:20 +00: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" )
}
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 21:25:49 +08: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
}