Improve template helper (#24417)
It seems that we really need the "context function" soon. So we should clean up the helper functions first. Major changes: * Improve StringUtils and add JsonUtils * Remove one-time-use helper functions like CompareLink * Move other code (no change) to util_avatar/util_render/util_misc (no need to propose changes for them) I have tested the changed templates:     --------- Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
		
					parent
					
						
							
								5a5ab8ef5a
							
						
					
				
			
			
				commit
				
					
						241b74f6c5
					
				
			
		
					 17 changed files with 650 additions and 571 deletions
				
			
		|  | @ -5,46 +5,25 @@ | |||
| package templates | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/hex" | ||||
| 	"fmt" | ||||
| 	"html" | ||||
| 	"html/template" | ||||
| 	"math" | ||||
| 	"mime" | ||||
| 	"net/url" | ||||
| 	"path/filepath" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 	"unicode" | ||||
| 
 | ||||
| 	activities_model "code.gitea.io/gitea/models/activities" | ||||
| 	"code.gitea.io/gitea/models/avatars" | ||||
| 	issues_model "code.gitea.io/gitea/models/issues" | ||||
| 	"code.gitea.io/gitea/models/organization" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	system_model "code.gitea.io/gitea/models/system" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| 	"code.gitea.io/gitea/modules/emoji" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	giturl "code.gitea.io/gitea/modules/git/url" | ||||
| 	gitea_html "code.gitea.io/gitea/modules/html" | ||||
| 	"code.gitea.io/gitea/modules/json" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/markup" | ||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | ||||
| 	"code.gitea.io/gitea/modules/repository" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/svg" | ||||
| 	"code.gitea.io/gitea/modules/templates/eval" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"code.gitea.io/gitea/services/gitdiff" | ||||
| 
 | ||||
| 	"github.com/editorconfig/editorconfig-core-go/v2" | ||||
| ) | ||||
| 
 | ||||
| // Used from static.go && dynamic.go | ||||
|  | @ -53,6 +32,8 @@ var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}[\s]*$`) | |||
| // NewFuncMap returns functions for injecting to templates | ||||
| func NewFuncMap() []template.FuncMap { | ||||
| 	return []template.FuncMap{map[string]interface{}{ | ||||
| 		"DumpVar": dumpVar, | ||||
| 
 | ||||
| 		// ----------------------------------------------------------------- | ||||
| 		// html/template related functions | ||||
| 		"dict":        dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names. | ||||
|  | @ -63,6 +44,7 @@ func NewFuncMap() []template.FuncMap { | |||
| 		"JSEscape":    template.JSEscapeString, | ||||
| 		"Str2html":    Str2html, // TODO: rename it to SanitizeHTML | ||||
| 		"URLJoin":     util.URLJoin, | ||||
| 		"DotEscape":   DotEscape, | ||||
| 
 | ||||
| 		"PathEscape":         url.PathEscape, | ||||
| 		"PathEscapeSegments": util.PathEscapeSegments, | ||||
|  | @ -70,30 +52,7 @@ func NewFuncMap() []template.FuncMap { | |||
| 		// utils | ||||
| 		"StringUtils": NewStringUtils, | ||||
| 		"SliceUtils":  NewSliceUtils, | ||||
| 
 | ||||
| 		// ----------------------------------------------------------------- | ||||
| 		// string / json | ||||
| 		// TODO: move string helper functions to StringUtils | ||||
| 		"Join":           strings.Join, | ||||
| 		"DotEscape":      DotEscape, | ||||
| 		"EllipsisString": base.EllipsisString, | ||||
| 		"DumpVar":        dumpVar, | ||||
| 
 | ||||
| 		"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() | ||||
| 		}, | ||||
| 		"JsonUtils":   NewJsonUtils, | ||||
| 
 | ||||
| 		// ----------------------------------------------------------------- | ||||
| 		// svg / avatar / icon | ||||
|  | @ -107,31 +66,7 @@ func NewFuncMap() []template.FuncMap { | |||
| 		"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 "" | ||||
| 		}, | ||||
| 		"SortArrow": SortArrow, | ||||
| 
 | ||||
| 		// ----------------------------------------------------------------- | ||||
| 		// time / number / format | ||||
|  | @ -242,32 +177,9 @@ func NewFuncMap() []template.FuncMap { | |||
| 		"ReactionToEmoji":  ReactionToEmoji, | ||||
| 		"RenderNote":       RenderNote, | ||||
| 
 | ||||
| 		"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) | ||||
| 			} | ||||
| 			return template.HTML(output) | ||||
| 		}, | ||||
| 		"RenderLabel": func(ctx context.Context, label *issues_model.Label) template.HTML { | ||||
| 			return template.HTML(RenderLabel(ctx, label)) | ||||
| 		}, | ||||
| 		"RenderLabels": func(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML { | ||||
| 			htmlCode := `<span class="labels-list">` | ||||
| 			for _, label := range labels { | ||||
| 				// Protect against nil value in labels - shouldn't happen but would cause a panic if so | ||||
| 				if label == nil { | ||||
| 					continue | ||||
| 				} | ||||
| 				htmlCode += fmt.Sprintf("<a href='%s/issues?labels=%d'>%s</a> ", | ||||
| 					repoLink, label.ID, RenderLabel(ctx, label)) | ||||
| 			} | ||||
| 			htmlCode += "</span>" | ||||
| 			return template.HTML(htmlCode) | ||||
| 		}, | ||||
| 		"RenderMarkdownToHtml": RenderMarkdownToHtml, | ||||
| 		"RenderLabel":          RenderLabel, | ||||
| 		"RenderLabels":         RenderLabels, | ||||
| 
 | ||||
| 		// ----------------------------------------------------------------- | ||||
| 		// misc | ||||
|  | @ -278,124 +190,11 @@ func NewFuncMap() []template.FuncMap { | |||
| 		"CommentMustAsDiff":        gitdiff.CommentMustAsDiff, | ||||
| 		"MirrorRemoteAddress":      mirrorRemoteAddress, | ||||
| 
 | ||||
| 		"ParseDeadline": func(deadline string) []string { | ||||
| 			return strings.Split(deadline, "|") | ||||
| 		}, | ||||
| 		"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) | ||||
| 				} | ||||
| 			} | ||||
| 			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 | ||||
| 		}, | ||||
| 		"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, | ||||
| 			) | ||||
| 		}, | ||||
| 		"FilenameIsImage": FilenameIsImage, | ||||
| 		"TabSizeClass":    TabSizeClass, | ||||
| 	}} | ||||
| } | ||||
| 
 | ||||
| // AvatarHTML creates the HTML for an avatar | ||||
| func AvatarHTML(src string, size int, class, name string) template.HTML { | ||||
| 	sizeStr := fmt.Sprintf(`%d`, size) | ||||
| 
 | ||||
| 	if name == "" { | ||||
| 		name = "avatar" | ||||
| 	} | ||||
| 
 | ||||
| 	return template.HTML(`<img class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`) | ||||
| } | ||||
| 
 | ||||
| // Avatar renders user avatars. args: user, size (int), class (string) | ||||
| func Avatar(ctx context.Context, item interface{}, others ...interface{}) template.HTML { | ||||
| 	size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...) | ||||
| 
 | ||||
| 	switch t := item.(type) { | ||||
| 	case *user_model.User: | ||||
| 		src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor) | ||||
| 		if src != "" { | ||||
| 			return AvatarHTML(src, size, class, t.DisplayName()) | ||||
| 		} | ||||
| 	case *repo_model.Collaborator: | ||||
| 		src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor) | ||||
| 		if src != "" { | ||||
| 			return AvatarHTML(src, size, class, t.DisplayName()) | ||||
| 		} | ||||
| 	case *organization.Organization: | ||||
| 		src := t.AsUser().AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor) | ||||
| 		if src != "" { | ||||
| 			return AvatarHTML(src, size, class, t.AsUser().DisplayName()) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return template.HTML("") | ||||
| } | ||||
| 
 | ||||
| // AvatarByAction renders user avatars from action. args: action, size (int), class (string) | ||||
| func AvatarByAction(ctx context.Context, action *activities_model.Action, others ...interface{}) template.HTML { | ||||
| 	action.LoadActUser(ctx) | ||||
| 	return Avatar(ctx, action.ActUser, others...) | ||||
| } | ||||
| 
 | ||||
| // RepoAvatar renders repo avatars. args: repo, size(int), class (string) | ||||
| func RepoAvatar(repo *repo_model.Repository, others ...interface{}) template.HTML { | ||||
| 	size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...) | ||||
| 
 | ||||
| 	src := repo.RelAvatarLink() | ||||
| 	if src != "" { | ||||
| 		return AvatarHTML(src, size, class, repo.FullName()) | ||||
| 	} | ||||
| 	return template.HTML("") | ||||
| } | ||||
| 
 | ||||
| // AvatarByEmail renders avatars by email address. args: email, name, size (int), class (string) | ||||
| func AvatarByEmail(ctx context.Context, email, name string, others ...interface{}) template.HTML { | ||||
| 	size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...) | ||||
| 	src := avatars.GenerateEmailAvatarFastLink(ctx, email, size*setting.Avatar.RenderedSizeFactor) | ||||
| 
 | ||||
| 	if src != "" { | ||||
| 		return AvatarHTML(src, size, class, name) | ||||
| 	} | ||||
| 
 | ||||
| 	return template.HTML("") | ||||
| } | ||||
| 
 | ||||
| // Safe render raw as HTML | ||||
| func Safe(raw string) template.HTML { | ||||
| 	return template.HTML(raw) | ||||
|  | @ -411,342 +210,6 @@ func DotEscape(raw string) string { | |||
| 	return strings.ReplaceAll(raw, ".", "\u200d.\u200d") | ||||
| } | ||||
| 
 | ||||
| // RenderCommitMessage renders commit message with XSS-safe and special links. | ||||
| func RenderCommitMessage(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML { | ||||
| 	return RenderCommitMessageLink(ctx, msg, urlPrefix, "", metas) | ||||
| } | ||||
| 
 | ||||
| // RenderCommitMessageLink renders commit message as a XXS-safe link to the provided | ||||
| // default url, handling for special links. | ||||
| func RenderCommitMessageLink(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML { | ||||
| 	cleanMsg := template.HTMLEscapeString(msg) | ||||
| 	// we can safely assume that it will not return any error, since there | ||||
| 	// shouldn't be any special HTML. | ||||
| 	fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ | ||||
| 		Ctx:         ctx, | ||||
| 		URLPrefix:   urlPrefix, | ||||
| 		DefaultLink: urlDefault, | ||||
| 		Metas:       metas, | ||||
| 	}, cleanMsg) | ||||
| 	if err != nil { | ||||
| 		log.Error("RenderCommitMessage: %v", err) | ||||
| 		return "" | ||||
| 	} | ||||
| 	msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n") | ||||
| 	if len(msgLines) == 0 { | ||||
| 		return template.HTML("") | ||||
| 	} | ||||
| 	return template.HTML(msgLines[0]) | ||||
| } | ||||
| 
 | ||||
| // RenderCommitMessageLinkSubject renders commit message as a XXS-safe link to | ||||
| // the provided default url, handling for special links without email to links. | ||||
| func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML { | ||||
| 	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("") | ||||
| 	} | ||||
| 
 | ||||
| 	// we can safely assume that it will not return any error, since there | ||||
| 	// shouldn't be any special HTML. | ||||
| 	renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{ | ||||
| 		Ctx:         ctx, | ||||
| 		URLPrefix:   urlPrefix, | ||||
| 		DefaultLink: urlDefault, | ||||
| 		Metas:       metas, | ||||
| 	}, template.HTMLEscapeString(msgLine)) | ||||
| 	if err != nil { | ||||
| 		log.Error("RenderCommitMessageSubject: %v", err) | ||||
| 		return template.HTML("") | ||||
| 	} | ||||
| 	return template.HTML(renderedMessage) | ||||
| } | ||||
| 
 | ||||
| // RenderCommitBody extracts the body of a commit message without its title. | ||||
| func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML { | ||||
| 	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("") | ||||
| 	} | ||||
| 
 | ||||
| 	renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ | ||||
| 		Ctx:       ctx, | ||||
| 		URLPrefix: urlPrefix, | ||||
| 		Metas:     metas, | ||||
| 	}, template.HTMLEscapeString(msgLine)) | ||||
| 	if err != nil { | ||||
| 		log.Error("RenderCommitMessage: %v", err) | ||||
| 		return "" | ||||
| 	} | ||||
| 	return template.HTML(renderedMessage) | ||||
| } | ||||
| 
 | ||||
| // 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) | ||||
| } | ||||
| 
 | ||||
| // RenderIssueTitle renders issue/pull title with defined post processors | ||||
| func RenderIssueTitle(ctx context.Context, text, urlPrefix string, metas map[string]string) template.HTML { | ||||
| 	renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{ | ||||
| 		Ctx:       ctx, | ||||
| 		URLPrefix: urlPrefix, | ||||
| 		Metas:     metas, | ||||
| 	}, template.HTMLEscapeString(text)) | ||||
| 	if err != nil { | ||||
| 		log.Error("RenderIssueTitle: %v", err) | ||||
| 		return template.HTML("") | ||||
| 	} | ||||
| 	return template.HTML(renderedText) | ||||
| } | ||||
| 
 | ||||
| // RenderLabel renders a label | ||||
| func RenderLabel(ctx context.Context, label *issues_model.Label) string { | ||||
| 	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>", | ||||
| 			textColor, label.Color, description, RenderEmoji(ctx, label.Name)) | ||||
| 	} | ||||
| 
 | ||||
| 	// Scoped label | ||||
| 	scopeText := RenderEmoji(ctx, labelScope) | ||||
| 	itemText := RenderEmoji(ctx, label.Name[len(labelScope)+1:]) | ||||
| 
 | ||||
| 	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 | ||||
| 		contrast := 0.01 + luminance*0.03 | ||||
| 		// 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) | ||||
| } | ||||
| 
 | ||||
| // RenderEmoji renders html text with emoji post processors | ||||
| func RenderEmoji(ctx context.Context, text string) template.HTML { | ||||
| 	renderedText, err := markup.RenderEmoji(&markup.RenderContext{Ctx: ctx}, | ||||
| 		template.HTMLEscapeString(text)) | ||||
| 	if err != nil { | ||||
| 		log.Error("RenderEmoji: %v", err) | ||||
| 		return template.HTML("") | ||||
| 	} | ||||
| 	return template.HTML(renderedText) | ||||
| } | ||||
| 
 | ||||
| // ReactionToEmoji renders emoji for use in reactions | ||||
| 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) | ||||
| 	} | ||||
| 	return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction))) | ||||
| } | ||||
| 
 | ||||
| // RenderNote renders the contents of a git-notes file as a commit message. | ||||
| func RenderNote(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML { | ||||
| 	cleanMsg := template.HTMLEscapeString(msg) | ||||
| 	fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ | ||||
| 		Ctx:       ctx, | ||||
| 		URLPrefix: urlPrefix, | ||||
| 		Metas:     metas, | ||||
| 	}, cleanMsg) | ||||
| 	if err != nil { | ||||
| 		log.Error("RenderNote: %v", err) | ||||
| 		return "" | ||||
| 	} | ||||
| 	return template.HTML(fullMessage) | ||||
| } | ||||
| 
 | ||||
| // IsMultilineCommitMessage checks to see if a commit message contains multiple lines. | ||||
| func IsMultilineCommitMessage(msg string) bool { | ||||
| 	return strings.Count(strings.TrimSpace(msg), "\n") >= 1 | ||||
| } | ||||
| 
 | ||||
| // Actioner describes an action | ||||
| type Actioner interface { | ||||
| 	GetOpType() activities_model.ActionType | ||||
| 	GetActUserName() string | ||||
| 	GetRepoUserName() string | ||||
| 	GetRepoName() string | ||||
| 	GetRepoPath() string | ||||
| 	GetRepoLink() string | ||||
| 	GetBranch() string | ||||
| 	GetContent() string | ||||
| 	GetCreate() time.Time | ||||
| 	GetIssueInfos() []string | ||||
| } | ||||
| 
 | ||||
| // ActionIcon accepts an action operation type and returns an icon class name. | ||||
| func ActionIcon(opType activities_model.ActionType) string { | ||||
| 	switch opType { | ||||
| 	case activities_model.ActionCreateRepo, activities_model.ActionTransferRepo, activities_model.ActionRenameRepo: | ||||
| 		return "repo" | ||||
| 	case activities_model.ActionCommitRepo, activities_model.ActionPushTag, activities_model.ActionDeleteTag, activities_model.ActionDeleteBranch: | ||||
| 		return "git-commit" | ||||
| 	case activities_model.ActionCreateIssue: | ||||
| 		return "issue-opened" | ||||
| 	case activities_model.ActionCreatePullRequest: | ||||
| 		return "git-pull-request" | ||||
| 	case activities_model.ActionCommentIssue, activities_model.ActionCommentPull: | ||||
| 		return "comment-discussion" | ||||
| 	case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest: | ||||
| 		return "git-merge" | ||||
| 	case activities_model.ActionCloseIssue, activities_model.ActionClosePullRequest: | ||||
| 		return "issue-closed" | ||||
| 	case activities_model.ActionReopenIssue, activities_model.ActionReopenPullRequest: | ||||
| 		return "issue-reopened" | ||||
| 	case activities_model.ActionMirrorSyncPush, activities_model.ActionMirrorSyncCreate, activities_model.ActionMirrorSyncDelete: | ||||
| 		return "mirror" | ||||
| 	case activities_model.ActionApprovePullRequest: | ||||
| 		return "check" | ||||
| 	case activities_model.ActionRejectPullRequest: | ||||
| 		return "diff" | ||||
| 	case activities_model.ActionPublishRelease: | ||||
| 		return "tag" | ||||
| 	case activities_model.ActionPullReviewDismissed: | ||||
| 		return "x" | ||||
| 	default: | ||||
| 		return "question" | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // ActionContent2Commits converts action content to push commits | ||||
| func ActionContent2Commits(act Actioner) *repository.PushCommits { | ||||
| 	push := repository.NewPushCommits() | ||||
| 
 | ||||
| 	if act == nil || act.GetContent() == "" { | ||||
| 		return push | ||||
| 	} | ||||
| 
 | ||||
| 	if err := json.Unmarshal([]byte(act.GetContent()), push); err != nil { | ||||
| 		log.Error("json.Unmarshal:\n%s\nERROR: %v", act.GetContent(), err) | ||||
| 	} | ||||
| 
 | ||||
| 	if push.Len == 0 { | ||||
| 		push.Len = len(push.Commits) | ||||
| 	} | ||||
| 
 | ||||
| 	return push | ||||
| } | ||||
| 
 | ||||
| // DiffLineTypeToStr returns diff line type name | ||||
| func DiffLineTypeToStr(diffType int) string { | ||||
| 	switch diffType { | ||||
| 	case 2: | ||||
| 		return "add" | ||||
| 	case 3: | ||||
| 		return "del" | ||||
| 	case 4: | ||||
| 		return "tag" | ||||
| 	} | ||||
| 	return "same" | ||||
| } | ||||
| 
 | ||||
| // MigrationIcon returns a SVG name matching the service an issue/comment was migrated from | ||||
| func MigrationIcon(hostname string) string { | ||||
| 	switch hostname { | ||||
| 	case "github.com": | ||||
| 		return "octicon-mark-github" | ||||
| 	default: | ||||
| 		return "gitea-git" | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type remoteAddress struct { | ||||
| 	Address  string | ||||
| 	Username string | ||||
| 	Password string | ||||
| } | ||||
| 
 | ||||
| func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string, ignoreOriginalURL bool) remoteAddress { | ||||
| 	a := remoteAddress{} | ||||
| 
 | ||||
| 	remoteURL := m.OriginalURL | ||||
| 	if ignoreOriginalURL || remoteURL == "" { | ||||
| 		var err error | ||||
| 		remoteURL, err = git.GetRemoteAddress(ctx, m.RepoPath(), remoteName) | ||||
| 		if err != nil { | ||||
| 			log.Error("GetRemoteURL %v", err) | ||||
| 			return a | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	u, err := giturl.Parse(remoteURL) | ||||
| 	if err != nil { | ||||
| 		log.Error("giturl.Parse %v", err) | ||||
| 		return a | ||||
| 	} | ||||
| 
 | ||||
| 	if u.Scheme != "ssh" && u.Scheme != "file" { | ||||
| 		if u.User != nil { | ||||
| 			a.Username = u.User.Username() | ||||
| 			a.Password, _ = u.User.Password() | ||||
| 		} | ||||
| 		u.User = nil | ||||
| 	} | ||||
| 	a.Address = u.String() | ||||
| 
 | ||||
| 	return a | ||||
| } | ||||
| 
 | ||||
| // 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. | ||||
| // | ||||
|  |  | |||
							
								
								
									
										84
									
								
								modules/templates/util_avatar.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								modules/templates/util_avatar.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,84 @@ | |||
| // Copyright 2023 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package templates | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"html" | ||||
| 	"html/template" | ||||
| 
 | ||||
| 	activities_model "code.gitea.io/gitea/models/activities" | ||||
| 	"code.gitea.io/gitea/models/avatars" | ||||
| 	"code.gitea.io/gitea/models/organization" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	gitea_html "code.gitea.io/gitea/modules/html" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| ) | ||||
| 
 | ||||
| // AvatarHTML creates the HTML for an avatar | ||||
| func AvatarHTML(src string, size int, class, name string) template.HTML { | ||||
| 	sizeStr := fmt.Sprintf(`%d`, size) | ||||
| 
 | ||||
| 	if name == "" { | ||||
| 		name = "avatar" | ||||
| 	} | ||||
| 
 | ||||
| 	return template.HTML(`<img class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`) | ||||
| } | ||||
| 
 | ||||
| // Avatar renders user avatars. args: user, size (int), class (string) | ||||
| func Avatar(ctx context.Context, item interface{}, others ...interface{}) template.HTML { | ||||
| 	size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...) | ||||
| 
 | ||||
| 	switch t := item.(type) { | ||||
| 	case *user_model.User: | ||||
| 		src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor) | ||||
| 		if src != "" { | ||||
| 			return AvatarHTML(src, size, class, t.DisplayName()) | ||||
| 		} | ||||
| 	case *repo_model.Collaborator: | ||||
| 		src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor) | ||||
| 		if src != "" { | ||||
| 			return AvatarHTML(src, size, class, t.DisplayName()) | ||||
| 		} | ||||
| 	case *organization.Organization: | ||||
| 		src := t.AsUser().AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor) | ||||
| 		if src != "" { | ||||
| 			return AvatarHTML(src, size, class, t.AsUser().DisplayName()) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return template.HTML("") | ||||
| } | ||||
| 
 | ||||
| // AvatarByAction renders user avatars from action. args: action, size (int), class (string) | ||||
| func AvatarByAction(ctx context.Context, action *activities_model.Action, others ...interface{}) template.HTML { | ||||
| 	action.LoadActUser(ctx) | ||||
| 	return Avatar(ctx, action.ActUser, others...) | ||||
| } | ||||
| 
 | ||||
| // RepoAvatar renders repo avatars. args: repo, size(int), class (string) | ||||
| func RepoAvatar(repo *repo_model.Repository, others ...interface{}) template.HTML { | ||||
| 	size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...) | ||||
| 
 | ||||
| 	src := repo.RelAvatarLink() | ||||
| 	if src != "" { | ||||
| 		return AvatarHTML(src, size, class, repo.FullName()) | ||||
| 	} | ||||
| 	return template.HTML("") | ||||
| } | ||||
| 
 | ||||
| // AvatarByEmail renders avatars by email address. args: email, name, size (int), class (string) | ||||
| func AvatarByEmail(ctx context.Context, email, name string, others ...interface{}) template.HTML { | ||||
| 	size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...) | ||||
| 	src := avatars.GenerateEmailAvatarFastLink(ctx, email, size*setting.Avatar.RenderedSizeFactor) | ||||
| 
 | ||||
| 	if src != "" { | ||||
| 		return AvatarHTML(src, size, class, name) | ||||
| 	} | ||||
| 
 | ||||
| 	return template.HTML("") | ||||
| } | ||||
							
								
								
									
										35
									
								
								modules/templates/util_json.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								modules/templates/util_json.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | |||
| // Copyright 2023 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package templates | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/json" | ||||
| ) | ||||
| 
 | ||||
| type JsonUtils struct{} //nolint:revive | ||||
| 
 | ||||
| var jsonUtils = JsonUtils{} | ||||
| 
 | ||||
| func NewJsonUtils() *JsonUtils { //nolint:revive | ||||
| 	return &jsonUtils | ||||
| } | ||||
| 
 | ||||
| func (su *JsonUtils) EncodeToString(v any) string { | ||||
| 	out, err := json.Marshal(v) | ||||
| 	if err != nil { | ||||
| 		return "" | ||||
| 	} | ||||
| 	return string(out) | ||||
| } | ||||
| 
 | ||||
| func (su *JsonUtils) PrettyIndent(s string) string { | ||||
| 	var out bytes.Buffer | ||||
| 	err := json.Indent(&out, []byte(s), "", "  ") | ||||
| 	if err != nil { | ||||
| 		return "" | ||||
| 	} | ||||
| 	return out.String() | ||||
| } | ||||
							
								
								
									
										209
									
								
								modules/templates/util_misc.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								modules/templates/util_misc.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,209 @@ | |||
| // Copyright 2023 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package templates | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"html/template" | ||||
| 	"mime" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	activities_model "code.gitea.io/gitea/models/activities" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	giturl "code.gitea.io/gitea/modules/git/url" | ||||
| 	"code.gitea.io/gitea/modules/json" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/repository" | ||||
| 	"code.gitea.io/gitea/modules/svg" | ||||
| 
 | ||||
| 	"github.com/editorconfig/editorconfig-core-go/v2" | ||||
| ) | ||||
| 
 | ||||
| func SortArrow(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 "" | ||||
| } | ||||
| 
 | ||||
| // IsMultilineCommitMessage checks to see if a commit message contains multiple lines. | ||||
| func IsMultilineCommitMessage(msg string) bool { | ||||
| 	return strings.Count(strings.TrimSpace(msg), "\n") >= 1 | ||||
| } | ||||
| 
 | ||||
| // Actioner describes an action | ||||
| type Actioner interface { | ||||
| 	GetOpType() activities_model.ActionType | ||||
| 	GetActUserName() string | ||||
| 	GetRepoUserName() string | ||||
| 	GetRepoName() string | ||||
| 	GetRepoPath() string | ||||
| 	GetRepoLink() string | ||||
| 	GetBranch() string | ||||
| 	GetContent() string | ||||
| 	GetCreate() time.Time | ||||
| 	GetIssueInfos() []string | ||||
| } | ||||
| 
 | ||||
| // ActionIcon accepts an action operation type and returns an icon class name. | ||||
| func ActionIcon(opType activities_model.ActionType) string { | ||||
| 	switch opType { | ||||
| 	case activities_model.ActionCreateRepo, activities_model.ActionTransferRepo, activities_model.ActionRenameRepo: | ||||
| 		return "repo" | ||||
| 	case activities_model.ActionCommitRepo, activities_model.ActionPushTag, activities_model.ActionDeleteTag, activities_model.ActionDeleteBranch: | ||||
| 		return "git-commit" | ||||
| 	case activities_model.ActionCreateIssue: | ||||
| 		return "issue-opened" | ||||
| 	case activities_model.ActionCreatePullRequest: | ||||
| 		return "git-pull-request" | ||||
| 	case activities_model.ActionCommentIssue, activities_model.ActionCommentPull: | ||||
| 		return "comment-discussion" | ||||
| 	case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest: | ||||
| 		return "git-merge" | ||||
| 	case activities_model.ActionCloseIssue, activities_model.ActionClosePullRequest: | ||||
| 		return "issue-closed" | ||||
| 	case activities_model.ActionReopenIssue, activities_model.ActionReopenPullRequest: | ||||
| 		return "issue-reopened" | ||||
| 	case activities_model.ActionMirrorSyncPush, activities_model.ActionMirrorSyncCreate, activities_model.ActionMirrorSyncDelete: | ||||
| 		return "mirror" | ||||
| 	case activities_model.ActionApprovePullRequest: | ||||
| 		return "check" | ||||
| 	case activities_model.ActionRejectPullRequest: | ||||
| 		return "diff" | ||||
| 	case activities_model.ActionPublishRelease: | ||||
| 		return "tag" | ||||
| 	case activities_model.ActionPullReviewDismissed: | ||||
| 		return "x" | ||||
| 	default: | ||||
| 		return "question" | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // ActionContent2Commits converts action content to push commits | ||||
| func ActionContent2Commits(act Actioner) *repository.PushCommits { | ||||
| 	push := repository.NewPushCommits() | ||||
| 
 | ||||
| 	if act == nil || act.GetContent() == "" { | ||||
| 		return push | ||||
| 	} | ||||
| 
 | ||||
| 	if err := json.Unmarshal([]byte(act.GetContent()), push); err != nil { | ||||
| 		log.Error("json.Unmarshal:\n%s\nERROR: %v", act.GetContent(), err) | ||||
| 	} | ||||
| 
 | ||||
| 	if push.Len == 0 { | ||||
| 		push.Len = len(push.Commits) | ||||
| 	} | ||||
| 
 | ||||
| 	return push | ||||
| } | ||||
| 
 | ||||
| // DiffLineTypeToStr returns diff line type name | ||||
| func DiffLineTypeToStr(diffType int) string { | ||||
| 	switch diffType { | ||||
| 	case 2: | ||||
| 		return "add" | ||||
| 	case 3: | ||||
| 		return "del" | ||||
| 	case 4: | ||||
| 		return "tag" | ||||
| 	} | ||||
| 	return "same" | ||||
| } | ||||
| 
 | ||||
| // MigrationIcon returns a SVG name matching the service an issue/comment was migrated from | ||||
| func MigrationIcon(hostname string) string { | ||||
| 	switch hostname { | ||||
| 	case "github.com": | ||||
| 		return "octicon-mark-github" | ||||
| 	default: | ||||
| 		return "gitea-git" | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type remoteAddress struct { | ||||
| 	Address  string | ||||
| 	Username string | ||||
| 	Password string | ||||
| } | ||||
| 
 | ||||
| func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string, ignoreOriginalURL bool) remoteAddress { | ||||
| 	a := remoteAddress{} | ||||
| 
 | ||||
| 	remoteURL := m.OriginalURL | ||||
| 	if ignoreOriginalURL || remoteURL == "" { | ||||
| 		var err error | ||||
| 		remoteURL, err = git.GetRemoteAddress(ctx, m.RepoPath(), remoteName) | ||||
| 		if err != nil { | ||||
| 			log.Error("GetRemoteURL %v", err) | ||||
| 			return a | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	u, err := giturl.Parse(remoteURL) | ||||
| 	if err != nil { | ||||
| 		log.Error("giturl.Parse %v", err) | ||||
| 		return a | ||||
| 	} | ||||
| 
 | ||||
| 	if u.Scheme != "ssh" && u.Scheme != "file" { | ||||
| 		if u.User != nil { | ||||
| 			a.Username = u.User.Username() | ||||
| 			a.Password, _ = u.User.Password() | ||||
| 		} | ||||
| 		u.User = nil | ||||
| 	} | ||||
| 	a.Address = u.String() | ||||
| 
 | ||||
| 	return a | ||||
| } | ||||
| 
 | ||||
| func FilenameIsImage(filename string) bool { | ||||
| 	mimeType := mime.TypeByExtension(filepath.Ext(filename)) | ||||
| 	return strings.HasPrefix(mimeType, "image/") | ||||
| } | ||||
| 
 | ||||
| func TabSizeClass(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) | ||||
| 		} | ||||
| 	} | ||||
| 	return "tab-size-8" | ||||
| } | ||||
							
								
								
									
										254
									
								
								modules/templates/util_render.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										254
									
								
								modules/templates/util_render.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,254 @@ | |||
| // Copyright 2023 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package templates | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/hex" | ||||
| 	"fmt" | ||||
| 	"html/template" | ||||
| 	"math" | ||||
| 	"net/url" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"unicode" | ||||
| 
 | ||||
| 	issues_model "code.gitea.io/gitea/models/issues" | ||||
| 	"code.gitea.io/gitea/modules/emoji" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/markup" | ||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| ) | ||||
| 
 | ||||
| // RenderCommitMessage renders commit message with XSS-safe and special links. | ||||
| func RenderCommitMessage(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML { | ||||
| 	return RenderCommitMessageLink(ctx, msg, urlPrefix, "", metas) | ||||
| } | ||||
| 
 | ||||
| // RenderCommitMessageLink renders commit message as a XXS-safe link to the provided | ||||
| // default url, handling for special links. | ||||
| func RenderCommitMessageLink(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML { | ||||
| 	cleanMsg := template.HTMLEscapeString(msg) | ||||
| 	// we can safely assume that it will not return any error, since there | ||||
| 	// shouldn't be any special HTML. | ||||
| 	fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ | ||||
| 		Ctx:         ctx, | ||||
| 		URLPrefix:   urlPrefix, | ||||
| 		DefaultLink: urlDefault, | ||||
| 		Metas:       metas, | ||||
| 	}, cleanMsg) | ||||
| 	if err != nil { | ||||
| 		log.Error("RenderCommitMessage: %v", err) | ||||
| 		return "" | ||||
| 	} | ||||
| 	msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n") | ||||
| 	if len(msgLines) == 0 { | ||||
| 		return template.HTML("") | ||||
| 	} | ||||
| 	return template.HTML(msgLines[0]) | ||||
| } | ||||
| 
 | ||||
| // RenderCommitMessageLinkSubject renders commit message as a XXS-safe link to | ||||
| // the provided default url, handling for special links without email to links. | ||||
| func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML { | ||||
| 	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("") | ||||
| 	} | ||||
| 
 | ||||
| 	// we can safely assume that it will not return any error, since there | ||||
| 	// shouldn't be any special HTML. | ||||
| 	renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{ | ||||
| 		Ctx:         ctx, | ||||
| 		URLPrefix:   urlPrefix, | ||||
| 		DefaultLink: urlDefault, | ||||
| 		Metas:       metas, | ||||
| 	}, template.HTMLEscapeString(msgLine)) | ||||
| 	if err != nil { | ||||
| 		log.Error("RenderCommitMessageSubject: %v", err) | ||||
| 		return template.HTML("") | ||||
| 	} | ||||
| 	return template.HTML(renderedMessage) | ||||
| } | ||||
| 
 | ||||
| // RenderCommitBody extracts the body of a commit message without its title. | ||||
| func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML { | ||||
| 	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("") | ||||
| 	} | ||||
| 
 | ||||
| 	renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ | ||||
| 		Ctx:       ctx, | ||||
| 		URLPrefix: urlPrefix, | ||||
| 		Metas:     metas, | ||||
| 	}, template.HTMLEscapeString(msgLine)) | ||||
| 	if err != nil { | ||||
| 		log.Error("RenderCommitMessage: %v", err) | ||||
| 		return "" | ||||
| 	} | ||||
| 	return template.HTML(renderedMessage) | ||||
| } | ||||
| 
 | ||||
| // 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) | ||||
| } | ||||
| 
 | ||||
| // RenderIssueTitle renders issue/pull title with defined post processors | ||||
| func RenderIssueTitle(ctx context.Context, text, urlPrefix string, metas map[string]string) template.HTML { | ||||
| 	renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{ | ||||
| 		Ctx:       ctx, | ||||
| 		URLPrefix: urlPrefix, | ||||
| 		Metas:     metas, | ||||
| 	}, template.HTMLEscapeString(text)) | ||||
| 	if err != nil { | ||||
| 		log.Error("RenderIssueTitle: %v", err) | ||||
| 		return template.HTML("") | ||||
| 	} | ||||
| 	return template.HTML(renderedText) | ||||
| } | ||||
| 
 | ||||
| // RenderLabel renders a label | ||||
| func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML { | ||||
| 	labelScope := label.ExclusiveScope() | ||||
| 
 | ||||
| 	textColor := "#111" | ||||
| 	if label.UseLightTextColor() { | ||||
| 		textColor = "#eee" | ||||
| 	} | ||||
| 
 | ||||
| 	description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description)) | ||||
| 
 | ||||
| 	if labelScope == "" { | ||||
| 		// Regular label | ||||
| 		s := fmt.Sprintf("<div class='ui label' style='color: %s !important; background-color: %s !important' title='%s'>%s</div>", | ||||
| 			textColor, label.Color, description, RenderEmoji(ctx, label.Name)) | ||||
| 		return template.HTML(s) | ||||
| 	} | ||||
| 
 | ||||
| 	// Scoped label | ||||
| 	scopeText := RenderEmoji(ctx, labelScope) | ||||
| 	itemText := RenderEmoji(ctx, label.Name[len(labelScope)+1:]) | ||||
| 
 | ||||
| 	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 | ||||
| 		contrast := 0.01 + luminance*0.03 | ||||
| 		// 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) | ||||
| 	} | ||||
| 
 | ||||
| 	s := 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) | ||||
| 	return template.HTML(s) | ||||
| } | ||||
| 
 | ||||
| // RenderEmoji renders html text with emoji post processors | ||||
| func RenderEmoji(ctx context.Context, text string) template.HTML { | ||||
| 	renderedText, err := markup.RenderEmoji(&markup.RenderContext{Ctx: ctx}, | ||||
| 		template.HTMLEscapeString(text)) | ||||
| 	if err != nil { | ||||
| 		log.Error("RenderEmoji: %v", err) | ||||
| 		return template.HTML("") | ||||
| 	} | ||||
| 	return template.HTML(renderedText) | ||||
| } | ||||
| 
 | ||||
| // ReactionToEmoji renders emoji for use in reactions | ||||
| 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) | ||||
| 	} | ||||
| 	return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction))) | ||||
| } | ||||
| 
 | ||||
| // RenderNote renders the contents of a git-notes file as a commit message. | ||||
| func RenderNote(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML { | ||||
| 	cleanMsg := template.HTMLEscapeString(msg) | ||||
| 	fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ | ||||
| 		Ctx:       ctx, | ||||
| 		URLPrefix: urlPrefix, | ||||
| 		Metas:     metas, | ||||
| 	}, cleanMsg) | ||||
| 	if err != nil { | ||||
| 		log.Error("RenderNote: %v", err) | ||||
| 		return "" | ||||
| 	} | ||||
| 	return template.HTML(fullMessage) | ||||
| } | ||||
| 
 | ||||
| func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //nolint:revive | ||||
| 	output, err := markdown.RenderString(&markup.RenderContext{ | ||||
| 		Ctx:       ctx, | ||||
| 		URLPrefix: setting.AppSubURL, | ||||
| 	}, input) | ||||
| 	if err != nil { | ||||
| 		log.Error("RenderString: %v", err) | ||||
| 	} | ||||
| 	return template.HTML(output) | ||||
| } | ||||
| 
 | ||||
| func RenderLabels(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML { | ||||
| 	htmlCode := `<span class="labels-list">` | ||||
| 	for _, label := range labels { | ||||
| 		// Protect against nil value in labels - shouldn't happen but would cause a panic if so | ||||
| 		if label == nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		htmlCode += fmt.Sprintf("<a href='%s/issues?labels=%d'>%s</a> ", | ||||
| 			repoLink, label.ID, RenderLabel(ctx, label)) | ||||
| 	} | ||||
| 	htmlCode += "</span>" | ||||
| 	return template.HTML(htmlCode) | ||||
| } | ||||
|  | @ -3,12 +3,18 @@ | |||
| 
 | ||||
| package templates | ||||
| 
 | ||||
| import "strings" | ||||
| import ( | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| ) | ||||
| 
 | ||||
| type StringUtils struct{} | ||||
| 
 | ||||
| var stringUtils = StringUtils{} | ||||
| 
 | ||||
| func NewStringUtils() *StringUtils { | ||||
| 	return &StringUtils{} | ||||
| 	return &stringUtils | ||||
| } | ||||
| 
 | ||||
| func (su *StringUtils) HasPrefix(s, prefix string) bool { | ||||
|  | @ -22,3 +28,11 @@ func (su *StringUtils) Contains(s, substr string) bool { | |||
| func (su *StringUtils) Split(s, sep string) []string { | ||||
| 	return strings.Split(s, sep) | ||||
| } | ||||
| 
 | ||||
| func (su *StringUtils) Join(a []string, sep string) string { | ||||
| 	return strings.Join(a, sep) | ||||
| } | ||||
| 
 | ||||
| func (su *StringUtils) EllipsisString(s string, max int) string { | ||||
| 	return base.EllipsisString(s, max) | ||||
| } | ||||
|  |  | |||
|  | @ -334,7 +334,7 @@ | |||
| 
 | ||||
| 					<div class="field"> | ||||
| 						<label for="oauth2_scopes">{{.locale.Tr "admin.auths.oauth2_scopes"}}</label> | ||||
| 						<input id="oauth2_scopes" name="oauth2_scopes" value="{{if $cfg.Scopes}}{{Join $cfg.Scopes ","}}{{end}}"> | ||||
| 						<input id="oauth2_scopes" name="oauth2_scopes" value="{{if $cfg.Scopes}}{{StringUtils.Join $cfg.Scopes ","}}{{end}}"> | ||||
| 					</div> | ||||
| 					<div class="field"> | ||||
| 						<label for="oauth2_required_claim_name">{{.locale.Tr "admin.auths.oauth2_required_claim_name"}}</label> | ||||
|  |  | |||
|  | @ -365,7 +365,7 @@ | |||
| 					<dt>{{$.locale.Tr "admin.config.log_mode"}}</dt> | ||||
| 					<dd>{{.Name}} ({{.Provider}})</dd> | ||||
| 					<dt>{{$.locale.Tr "admin.config.log_config"}}</dt> | ||||
| 					<dd><pre>{{.Config | JsonPrettyPrint}}</pre></dd> | ||||
| 					<dd><pre>{{JsonUtils.PrettyIndent .Config}}</pre></dd> | ||||
| 				{{end}} | ||||
| 				<div class="ui divider"></div> | ||||
| 				<dt>{{$.locale.Tr "admin.config.router_log_mode"}}</dt> | ||||
|  | @ -378,7 +378,7 @@ | |||
| 							<dt>{{$.locale.Tr "admin.config.log_mode"}}</dt> | ||||
| 							<dd>{{.Name}} ({{.Provider}})</dd> | ||||
| 							<dt>{{$.locale.Tr "admin.config.log_config"}}</dt> | ||||
| 							<dd><pre>{{.Config | JsonPrettyPrint}}</pre></dd> | ||||
| 							<dd><pre>{{JsonUtils.PrettyIndent .Config}}</pre></dd> | ||||
| 						{{end}} | ||||
| 					{{else}} | ||||
| 						<dd>{{$.locale.Tr "admin.config.routes_to_default_logger"}}</dd> | ||||
|  | @ -393,7 +393,7 @@ | |||
| 							<dt>{{$.locale.Tr "admin.config.log_mode"}}</dt> | ||||
| 							<dd>{{.Name}} ({{.Provider}})</dd> | ||||
| 							<dt>{{$.locale.Tr "admin.config.log_config"}}</dt> | ||||
| 							<dd><pre>{{.Config | JsonPrettyPrint}}</pre></dd> | ||||
| 							<dd><pre>{{JsonUtils.PrettyIndent .Config}}</pre></dd> | ||||
| 						{{end}} | ||||
| 					{{else}} | ||||
| 						<dd>{{$.locale.Tr "admin.config.routes_to_default_logger"}}</dd> | ||||
|  | @ -412,7 +412,7 @@ | |||
| 							<dt>{{$.locale.Tr "admin.config.log_mode"}}</dt> | ||||
| 							<dd>{{.Name}} ({{.Provider}})</dd> | ||||
| 							<dt>{{$.locale.Tr "admin.config.log_config"}}</dt> | ||||
| 							<dd><pre>{{.Config | JsonPrettyPrint}}</pre></dd> | ||||
| 							<dd><pre>{{JsonUtils.PrettyIndent .Config}}</pre></dd> | ||||
| 						{{end}} | ||||
| 					{{else}} | ||||
| 						<dd>{{$.locale.Tr "admin.config.routes_to_default_logger"}}</dd> | ||||
|  |  | |||
|  | @ -174,7 +174,7 @@ | |||
| 			{{.locale.Tr "admin.monitor.queue.configuration"}} | ||||
| 		</h4> | ||||
| 		<div class="ui attached segment"> | ||||
| 			<pre>{{.Queue.Configuration | JsonPrettyPrint}}</pre> | ||||
| 			<pre>{{JsonUtils.PrettyIndent .Queue.Configuration}}</pre> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 
 | ||||
|  |  | |||
|  | @ -22,9 +22,9 @@ | |||
| 					<a class="item" href="{{$.Link}}/rules/{{.ID}}"><strong>{{.Type.Name}}</strong></a> | ||||
| 					<div><i>{{if .Enabled}}{{$.locale.Tr "enabled"}}{{else}}{{$.locale.Tr "disabled"}}{{end}}</i></div> | ||||
| 					{{if .KeepCount}}<div><i>{{$.locale.Tr "packages.owner.settings.cleanuprules.keep.count"}}:</i> {{if eq .KeepCount 1}}{{$.locale.Tr "packages.owner.settings.cleanuprules.keep.count.1"}}{{else}}{{$.locale.Tr "packages.owner.settings.cleanuprules.keep.count.n" .KeepCount}}{{end}}</div>{{end}} | ||||
| 					{{if .KeepPattern}}<div><i>{{$.locale.Tr "packages.owner.settings.cleanuprules.keep.pattern"}}:</i> {{EllipsisString .KeepPattern 100}}</div>{{end}} | ||||
| 					{{if .KeepPattern}}<div><i>{{$.locale.Tr "packages.owner.settings.cleanuprules.keep.pattern"}}:</i> {{StringUtils.EllipsisString .KeepPattern 100}}</div>{{end}} | ||||
| 					{{if .RemoveDays}}<div><i>{{$.locale.Tr "packages.owner.settings.cleanuprules.remove.days"}}:</i> {{$.locale.Tr "tool.days" .RemoveDays}}</div>{{end}} | ||||
| 					{{if .RemovePattern}}<div><i>{{$.locale.Tr "packages.owner.settings.cleanuprules.remove.pattern"}}:</i> {{EllipsisString .RemovePattern 100}}</div>{{end}} | ||||
| 					{{if .RemovePattern}}<div><i>{{$.locale.Tr "packages.owner.settings.cleanuprules.remove.pattern"}}:</i> {{StringUtils.EllipsisString .RemovePattern 100}}</div>{{end}} | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{{else}} | ||||
|  |  | |||
|  | @ -68,7 +68,13 @@ | |||
| 				{{$l := Eval $n "-" 1}} | ||||
| 				<!-- If home page, show new pr. If not, show breadcrumb --> | ||||
| 				{{if and (eq $n 0) .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}} | ||||
| 					<a id="new-pull-request" role="button" class="ui compact basic button" href="{{CompareLink .BaseRepo .Repository .BranchName}}" | ||||
| 					{{$cmpBranch := ""}} | ||||
| 					{{if ne .Repository.ID .BaseRepo.ID}} | ||||
| 						{{$cmpBranch = printf "%s/%s:" (.Repository.OwnerName|PathEscape) (.Repository.Name|PathEscape)}} | ||||
| 					{{end}} | ||||
| 					{{$cmpBranch = printf "%s%s" $cmpBranch (.BranchName|PathEscapeSegments)}} | ||||
| 					{{$compareLink := printf "%s/compare/%s...%s" .BaseRepo.Link (.BaseRepo.DefaultBranch|PathEscapeSegments) $cmpBranch}} | ||||
| 					<a id="new-pull-request" role="button" class="ui compact basic button" href="{{$compareLink}}" | ||||
| 						data-tooltip-content="{{if .PullRequestCtx.Allowed}}{{.locale.Tr "repo.pulls.compare_changes"}}{{else}}{{.locale.Tr "action.compare_branch"}}{{end}}"> | ||||
| 						{{svg "octicon-git-pull-request"}} | ||||
| 					</a> | ||||
|  | @ -103,7 +109,17 @@ | |||
| 					</a> | ||||
| 				{{end}} | ||||
| 				{{if ne $n 0}} | ||||
| 					<span class="ui breadcrumb repo-path gt-ml-2"><a class="section" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}" title="{{.Repository.Name}}">{{EllipsisString .Repository.Name 30}}</a>{{range $i, $v := .TreeNames}}<span class="divider">/</span>{{if eq $i $l}}<span class="active section" title="{{$v}}">{{EllipsisString $v 30}}</span>{{else}}{{$p := index $.Paths $i}}<span class="section"><a href="{{$.BranchLink}}/{{PathEscapeSegments $p}}" title="{{$v}}">{{EllipsisString $v 30}}</a></span>{{end}}{{end}}</span> | ||||
| 					<span class="ui breadcrumb repo-path gt-ml-2"> | ||||
| 						<a class="section" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a> | ||||
| 						{{- range $i, $v := .TreeNames -}} | ||||
| 							<span class="divider">/</span> | ||||
| 							{{- if eq $i $l -}} | ||||
| 								<span class="active section" title="{{$v}}">{{StringUtils.EllipsisString $v 30}}</span> | ||||
| 							{{- else -}} | ||||
| 								{{$p := index $.Paths $i}}<span class="section"><a href="{{$.BranchLink}}/{{PathEscapeSegments $p}}" title="{{$v}}">{{StringUtils.EllipsisString $v 30}}</a></span> | ||||
| 							{{- end -}} | ||||
| 						{{- end -}} | ||||
| 					</span> | ||||
| 				{{end}} | ||||
| 			</div> | ||||
| 			<div class="gt-df gt-ac"> | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ | |||
| 					<div class="field"> | ||||
| 						<input name="title" id="issue_title" placeholder="{{.locale.Tr "repo.milestones.title"}}" value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}" tabindex="3" autofocus required maxlength="255" autocomplete="off"> | ||||
| 						{{if .PageIsComparePull}} | ||||
| 							<div class="title_wip_desc" data-wip-prefixes="{{Json .PullRequestWorkInProgressPrefixes}}">{{.locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0| Escape) | Safe}}</div> | ||||
| 							<div class="title_wip_desc" data-wip-prefixes="{{JsonUtils.EncodeToString .PullRequestWorkInProgressPrefixes}}">{{.locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0| Escape) | Safe}}</div> | ||||
| 						{{end}} | ||||
| 					</div> | ||||
| 					{{if .Fields}} | ||||
|  |  | |||
|  | @ -304,10 +304,12 @@ | |||
| 				{{template "shared/user/avatarlink" dict "Context" $.Context "user" .Poster}} | ||||
| 				<span class="text grey muted-links"> | ||||
| 					{{template "shared/user/authorlink" .Poster}} | ||||
| 					{{$parsedDeadline := .Content | ParseDeadline}} | ||||
| 					{{$from := DateTime "long" (index $parsedDeadline 1)}} | ||||
| 					{{$to := DateTime "long" (index $parsedDeadline 0)}} | ||||
| 					{{$.locale.Tr "repo.issues.due_date_modified" $to $from $createdStr | Safe}} | ||||
| 					{{$parsedDeadline := StringUtils.Split .Content "|"}} | ||||
| 					{{if eq (len $parsedDeadline) 2}} | ||||
| 						{{$from := DateTime "long" (index $parsedDeadline 1)}} | ||||
| 						{{$to := DateTime "long" (index $parsedDeadline 0)}} | ||||
| 						{{$.locale.Tr "repo.issues.due_date_modified" $to $from $createdStr | Safe}} | ||||
| 					{{end}} | ||||
| 				</span> | ||||
| 			</div> | ||||
| 		{{else if eq .Type 18}} | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ | |||
| 						<b>{{.tag_name}}</b><span class="at">@</span><strong>{{.tag_target}}</strong> | ||||
| 					{{else}} | ||||
| 						<input id="tag-name" name="tag_name" value="{{.tag_name}}" aria-label="{{.locale.Tr "repo.release.tag_name"}}" placeholder="{{.locale.Tr "repo.release.tag_name"}}" autofocus required maxlength="255"> | ||||
| 						<input id="tag-name-editor" type="hidden" data-existing-tags={{Json .Tags}} data-tag-helper={{.locale.Tr "repo.release.tag_helper"}} data-tag-helper-new={{.locale.Tr "repo.release.tag_helper_new"}} data-tag-helper-existing={{.locale.Tr "repo.release.tag_helper_existing"}}> | ||||
| 						<input id="tag-name-editor" type="hidden" data-existing-tags="{{JsonUtils.EncodeToString .Tags}}" data-tag-helper="{{.locale.Tr "repo.release.tag_helper"}}" data-tag-helper-new="{{.locale.Tr "repo.release.tag_helper_new"}}" data-tag-helper-existing="{{.locale.Tr "repo.release.tag_helper_existing"}}"> | ||||
| 						<div id="tag-target-selector" class="gt-dib"> | ||||
| 							<span class="at">@</span> | ||||
| 							<div class="ui selection dropdown"> | ||||
|  |  | |||
|  | @ -61,13 +61,15 @@ | |||
| 						{{else}} | ||||
| 							{{if $entry.IsDir}} | ||||
| 								{{$subJumpablePathName := $entry.GetSubJumpablePathName}} | ||||
| 								{{$subJumpablePath := SubJumpablePath $subJumpablePathName}} | ||||
| 								{{svg "octicon-file-directory-fill"}} | ||||
| 								<a class="muted" href="{{$.TreeLink}}/{{PathEscapeSegments $subJumpablePathName}}" title="{{$subJumpablePathName}}"> | ||||
| 									{{if eq (len $subJumpablePath) 2}} | ||||
| 										<span class="color-text-light-2">{{index  $subJumpablePath 0}}</span>{{index  $subJumpablePath 1}} | ||||
| 									{{$subJumpablePathFields := StringUtils.Split $subJumpablePathName "/"}} | ||||
| 									{{$subJumpablePathFieldLast := (Eval (len $subJumpablePathFields) "-" 1)}} | ||||
| 									{{if eq $subJumpablePathFieldLast 0}} | ||||
| 										{{$subJumpablePathName}} | ||||
| 									{{else}} | ||||
| 										{{index $subJumpablePath 0}} | ||||
| 										{{$subJumpablePathPrefixes := slice $subJumpablePathFields 0 $subJumpablePathFieldLast}} | ||||
| 										<span class="color-text-light-2">{{StringUtils.Join $subJumpablePathPrefixes "/"}}</span>/{{index $subJumpablePathFields $subJumpablePathFieldLast}} | ||||
| 									{{end}} | ||||
| 								</a> | ||||
| 							{{else}} | ||||
|  |  | |||
|  | @ -37,7 +37,7 @@ | |||
| 			</div> | ||||
| 			<div class="field" data-tooltip-content="Labels are comma-separated. Whitespace at the beginning, end, and around the commas are ignored."> | ||||
| 				<label for="custom_labels">{{.locale.Tr "actions.runners.custom_labels"}}</label> | ||||
| 				<input id="custom_labels" name="custom_labels" value="{{Join .Runner.CustomLabels `,`}}"> | ||||
| 				<input id="custom_labels" name="custom_labels" value="{{StringUtils.Join .Runner.CustomLabels `,`}}"> | ||||
| 				<p class="help">{{.locale.Tr "actions.runners.custom_labels_helper"}}</p> | ||||
| 			</div> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| {{if .HeatmapData}} | ||||
| 	<div id="user-heatmap" | ||||
| 		data-heatmap-data="{{Json .HeatmapData}}" | ||||
| 		data-heatmap-data="{{JsonUtils.EncodeToString .HeatmapData}}" | ||||
| 		data-locale-total-contributions="{{$.locale.Tr "heatmap.number_of_contributions_in_the_last_12_months" ($.locale.PrettyNumber .HeatmapTotalContributions)}}" | ||||
| 		data-locale-no-contributions="{{.locale.Tr "heatmap.no_contributions"}}" | ||||
| 		data-locale-more="{{.locale.Tr "heatmap.more"}}" | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 wxiaoguang
				wxiaoguang