mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 19:06:18 +01:00 
			
		
		
		
	Add material icons for file list (#33837)
This commit is contained in:
		| @@ -1294,6 +1294,9 @@ LEVEL = Info | |||||||
| ;; Leave it empty to allow users to select any theme from "{CustomPath}/public/assets/css/theme-*.css" | ;; Leave it empty to allow users to select any theme from "{CustomPath}/public/assets/css/theme-*.css" | ||||||
| ;THEMES = | ;THEMES = | ||||||
| ;; | ;; | ||||||
|  | ;; The icons for file list (basic/material), this is a temporary option which will be replaced by a user setting in the future. | ||||||
|  | ;FILE_ICON_THEME = material | ||||||
|  | ;; | ||||||
| ;; All available reactions users can choose on issues/prs and comments. | ;; All available reactions users can choose on issues/prs and comments. | ||||||
| ;; Values can be emoji alias (:smile:) or a unicode emoji. | ;; Values can be emoji alias (:smile:) or a unicode emoji. | ||||||
| ;; For custom reactions, add a tightly cropped square image to public/assets/img/emoji/reaction_name.png | ;; For custom reactions, add a tightly cropped square image to public/assets/img/emoji/reaction_name.png | ||||||
|   | |||||||
| @@ -17,7 +17,6 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/modules/git" |  | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
|  |  | ||||||
| @@ -139,24 +138,3 @@ func Int64sToStrings(ints []int64) []string { | |||||||
| 	} | 	} | ||||||
| 	return strs | 	return strs | ||||||
| } | } | ||||||
|  |  | ||||||
| // EntryIcon returns the octicon name for displaying files/directories |  | ||||||
| func EntryIcon(entry *git.TreeEntry) string { |  | ||||||
| 	switch { |  | ||||||
| 	case entry.IsLink(): |  | ||||||
| 		te, err := entry.FollowLink() |  | ||||||
| 		if err != nil { |  | ||||||
| 			return "file-symlink-file" |  | ||||||
| 		} |  | ||||||
| 		if te.IsDir() { |  | ||||||
| 			return "file-directory-symlink" |  | ||||||
| 		} |  | ||||||
| 		return "file-symlink-file" |  | ||||||
| 	case entry.IsDir(): |  | ||||||
| 		return "file-directory-fill" |  | ||||||
| 	case entry.IsSubModule(): |  | ||||||
| 		return "file-submodule" |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return "file" |  | ||||||
| } |  | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								modules/fileicon/basic.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								modules/fileicon/basic.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | // Copyright 2025 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package fileicon | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"html/template" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/modules/git" | ||||||
|  | 	"code.gitea.io/gitea/modules/svg" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func BasicThemeIcon(entry *git.TreeEntry) template.HTML { | ||||||
|  | 	svgName := "octicon-file" | ||||||
|  | 	switch { | ||||||
|  | 	case entry.IsLink(): | ||||||
|  | 		svgName = "octicon-file-symlink-file" | ||||||
|  | 		if te, err := entry.FollowLink(); err == nil && te.IsDir() { | ||||||
|  | 			svgName = "octicon-file-directory-symlink" | ||||||
|  | 		} | ||||||
|  | 	case entry.IsDir(): | ||||||
|  | 		svgName = "octicon-file-directory-fill" | ||||||
|  | 	case entry.IsSubModule(): | ||||||
|  | 		svgName = "octicon-file-submodule" | ||||||
|  | 	} | ||||||
|  | 	return svg.RenderHTML(svgName) | ||||||
|  | } | ||||||
							
								
								
									
										150
									
								
								modules/fileicon/material.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								modules/fileicon/material.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,150 @@ | |||||||
|  | // Copyright 2025 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package fileicon | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"html/template" | ||||||
|  | 	"path" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/modules/git" | ||||||
|  | 	"code.gitea.io/gitea/modules/json" | ||||||
|  | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 	"code.gitea.io/gitea/modules/options" | ||||||
|  | 	"code.gitea.io/gitea/modules/reqctx" | ||||||
|  | 	"code.gitea.io/gitea/modules/svg" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type materialIconRulesData struct { | ||||||
|  | 	IconDefinitions map[string]*struct { | ||||||
|  | 		IconPath string `json:"iconPath"` | ||||||
|  | 	} `json:"iconDefinitions"` | ||||||
|  | 	FileNames      map[string]string `json:"fileNames"` | ||||||
|  | 	FolderNames    map[string]string `json:"folderNames"` | ||||||
|  | 	FileExtensions map[string]string `json:"fileExtensions"` | ||||||
|  | 	LanguageIDs    map[string]string `json:"languageIds"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type MaterialIconProvider struct { | ||||||
|  | 	once  sync.Once | ||||||
|  | 	rules *materialIconRulesData | ||||||
|  | 	svgs  map[string]string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var materialIconProvider MaterialIconProvider | ||||||
|  |  | ||||||
|  | func DefaultMaterialIconProvider() *MaterialIconProvider { | ||||||
|  | 	return &materialIconProvider | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *MaterialIconProvider) loadData() { | ||||||
|  | 	buf, err := options.AssetFS().ReadFile("fileicon/material-icon-rules.json") | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("Failed to read material icon rules: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	err = json.Unmarshal(buf, &m.rules) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("Failed to unmarshal material icon rules: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	buf, err = options.AssetFS().ReadFile("fileicon/material-icon-svgs.json") | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("Failed to read material icon rules: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	err = json.Unmarshal(buf, &m.svgs) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("Failed to unmarshal material icon rules: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	log.Debug("Loaded material icon rules and SVG images") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *MaterialIconProvider) renderFileIconSVG(ctx reqctx.RequestContext, name, svg string) template.HTML { | ||||||
|  | 	data := ctx.GetData() | ||||||
|  | 	renderedSVGs, _ := data["_RenderedSVGs"].(map[string]bool) | ||||||
|  | 	if renderedSVGs == nil { | ||||||
|  | 		renderedSVGs = make(map[string]bool) | ||||||
|  | 		data["_RenderedSVGs"] = renderedSVGs | ||||||
|  | 	} | ||||||
|  | 	// This part is a bit hacky, but it works really well. It should be safe to do so because all SVG icons are generated by us. | ||||||
|  | 	// Will try to refactor this in the future. | ||||||
|  | 	if !strings.HasPrefix(svg, "<svg") { | ||||||
|  | 		panic("Invalid SVG icon") | ||||||
|  | 	} | ||||||
|  | 	svgID := "svg-mfi-" + name | ||||||
|  | 	svgCommonAttrs := `class="svg fileicon" width="16" height="16" aria-hidden="true"` | ||||||
|  | 	posOuterBefore := strings.IndexByte(svg, '>') | ||||||
|  | 	if renderedSVGs[svgID] && posOuterBefore != -1 { | ||||||
|  | 		return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`) | ||||||
|  | 	} | ||||||
|  | 	svg = `<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:] | ||||||
|  | 	renderedSVGs[svgID] = true | ||||||
|  | 	return template.HTML(svg) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *MaterialIconProvider) FileIcon(ctx reqctx.RequestContext, entry *git.TreeEntry) template.HTML { | ||||||
|  | 	m.once.Do(m.loadData) | ||||||
|  |  | ||||||
|  | 	if m.rules == nil { | ||||||
|  | 		return BasicThemeIcon(entry) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if entry.IsLink() { | ||||||
|  | 		if te, err := entry.FollowLink(); err == nil && te.IsDir() { | ||||||
|  | 			return svg.RenderHTML("material-folder-symlink") | ||||||
|  | 		} | ||||||
|  | 		return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	name := m.findIconName(entry) | ||||||
|  | 	if name == "folder" { | ||||||
|  | 		// the material icon pack's "folder" icon doesn't look good, so use our built-in one | ||||||
|  | 		return svg.RenderHTML("material-folder-generic") | ||||||
|  | 	} | ||||||
|  | 	if iconSVG, ok := m.svgs[name]; ok && iconSVG != "" { | ||||||
|  | 		return m.renderFileIconSVG(ctx, name, iconSVG) | ||||||
|  | 	} | ||||||
|  | 	return svg.RenderHTML("octicon-file") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *MaterialIconProvider) findIconName(entry *git.TreeEntry) string { | ||||||
|  | 	if entry.IsSubModule() { | ||||||
|  | 		return "folder-git" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	iconsData := m.rules | ||||||
|  | 	fileName := path.Base(entry.Name()) | ||||||
|  |  | ||||||
|  | 	if entry.IsDir() { | ||||||
|  | 		if s, ok := iconsData.FolderNames[fileName]; ok { | ||||||
|  | 			return s | ||||||
|  | 		} | ||||||
|  | 		if s, ok := iconsData.FolderNames[strings.ToLower(fileName)]; ok { | ||||||
|  | 			return s | ||||||
|  | 		} | ||||||
|  | 		return "folder" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if s, ok := iconsData.FileNames[fileName]; ok { | ||||||
|  | 		return s | ||||||
|  | 	} | ||||||
|  | 	if s, ok := iconsData.FileNames[strings.ToLower(fileName)]; ok { | ||||||
|  | 		return s | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for i := len(fileName) - 1; i >= 0; i-- { | ||||||
|  | 		if fileName[i] == '.' { | ||||||
|  | 			ext := fileName[i+1:] | ||||||
|  | 			if s, ok := iconsData.FileExtensions[ext]; ok { | ||||||
|  | 				return s | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return "file" | ||||||
|  | } | ||||||
| @@ -94,6 +94,9 @@ type RequestContext interface { | |||||||
| } | } | ||||||
|  |  | ||||||
| func FromContext(ctx context.Context) RequestContext { | func FromContext(ctx context.Context) RequestContext { | ||||||
|  | 	if rc, ok := ctx.(RequestContext); ok { | ||||||
|  | 		return rc | ||||||
|  | 	} | ||||||
| 	// here we must use the current ctx and the underlying store | 	// here we must use the current ctx and the underlying store | ||||||
| 	// the current ctx guarantees that the ctx deadline/cancellation/values are respected | 	// the current ctx guarantees that the ctx deadline/cancellation/values are respected | ||||||
| 	// the underlying store guarantees that the request-specific data is available | 	// the underlying store guarantees that the request-specific data is available | ||||||
| @@ -134,6 +137,6 @@ func NewRequestContext(parentCtx context.Context, profDesc string) (_ context.Co | |||||||
|  |  | ||||||
| // NewRequestContextForTest creates a new RequestContext for testing purposes | // NewRequestContextForTest creates a new RequestContext for testing purposes | ||||||
| // It doesn't add the context to the process manager, nor do cleanup | // It doesn't add the context to the process manager, nor do cleanup | ||||||
| func NewRequestContextForTest(parentCtx context.Context) context.Context { | func NewRequestContextForTest(parentCtx context.Context) RequestContext { | ||||||
| 	return &requestContext{Context: parentCtx, RequestDataStore: &requestDataStore{values: make(map[any]any)}} | 	return &requestContext{Context: parentCtx, RequestDataStore: &requestDataStore{values: make(map[any]any)}} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -28,6 +28,7 @@ var UI = struct { | |||||||
| 	DefaultShowFullName     bool | 	DefaultShowFullName     bool | ||||||
| 	DefaultTheme            string | 	DefaultTheme            string | ||||||
| 	Themes                  []string | 	Themes                  []string | ||||||
|  | 	FileIconTheme           string | ||||||
| 	Reactions               []string | 	Reactions               []string | ||||||
| 	ReactionsLookup         container.Set[string] `ini:"-"` | 	ReactionsLookup         container.Set[string] `ini:"-"` | ||||||
| 	CustomEmojis            []string | 	CustomEmojis            []string | ||||||
| @@ -84,6 +85,7 @@ var UI = struct { | |||||||
| 	ReactionMaxUserNum:      10, | 	ReactionMaxUserNum:      10, | ||||||
| 	MaxDisplayFileSize:      8388608, | 	MaxDisplayFileSize:      8388608, | ||||||
| 	DefaultTheme:            `gitea-auto`, | 	DefaultTheme:            `gitea-auto`, | ||||||
|  | 	FileIconTheme:           `material`, | ||||||
| 	Reactions:               []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, | 	Reactions:               []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, | ||||||
| 	CustomEmojis:            []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`}, | 	CustomEmojis:            []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`}, | ||||||
| 	CustomEmojisMap:         map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"}, | 	CustomEmojisMap:         map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"}, | ||||||
|   | |||||||
| @@ -59,7 +59,6 @@ func NewFuncMap() template.FuncMap { | |||||||
| 		// ----------------------------------------------------------------- | 		// ----------------------------------------------------------------- | ||||||
| 		// svg / avatar / icon / color | 		// svg / avatar / icon / color | ||||||
| 		"svg":           svg.RenderHTML, | 		"svg":           svg.RenderHTML, | ||||||
| 		"EntryIcon":     base.EntryIcon, |  | ||||||
| 		"MigrationIcon": migrationIcon, | 		"MigrationIcon": migrationIcon, | ||||||
| 		"ActionIcon":    actionIcon, | 		"ActionIcon":    actionIcon, | ||||||
| 		"SortArrow":     sortArrow, | 		"SortArrow":     sortArrow, | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ | |||||||
| package templates | package templates | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"encoding/hex" | 	"encoding/hex" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"html/template" | 	"html/template" | ||||||
| @@ -16,20 +15,23 @@ import ( | |||||||
|  |  | ||||||
| 	issues_model "code.gitea.io/gitea/models/issues" | 	issues_model "code.gitea.io/gitea/models/issues" | ||||||
| 	"code.gitea.io/gitea/modules/emoji" | 	"code.gitea.io/gitea/modules/emoji" | ||||||
|  | 	"code.gitea.io/gitea/modules/fileicon" | ||||||
|  | 	"code.gitea.io/gitea/modules/git" | ||||||
| 	"code.gitea.io/gitea/modules/htmlutil" | 	"code.gitea.io/gitea/modules/htmlutil" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/markup" | 	"code.gitea.io/gitea/modules/markup" | ||||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | 	"code.gitea.io/gitea/modules/markup/markdown" | ||||||
|  | 	"code.gitea.io/gitea/modules/reqctx" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/translation" | 	"code.gitea.io/gitea/modules/translation" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type RenderUtils struct { | type RenderUtils struct { | ||||||
| 	ctx context.Context | 	ctx reqctx.RequestContext | ||||||
| } | } | ||||||
|  |  | ||||||
| func NewRenderUtils(ctx context.Context) *RenderUtils { | func NewRenderUtils(ctx reqctx.RequestContext) *RenderUtils { | ||||||
| 	return &RenderUtils{ctx: ctx} | 	return &RenderUtils{ctx: ctx} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -179,6 +181,13 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML { | |||||||
| 		textColor, itemColor, itemHTML) | 		textColor, itemColor, itemHTML) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (ut *RenderUtils) RenderFileIcon(entry *git.TreeEntry) template.HTML { | ||||||
|  | 	if setting.UI.FileIconTheme == "material" { | ||||||
|  | 		return fileicon.DefaultMaterialIconProvider().FileIcon(ut.ctx, entry) | ||||||
|  | 	} | ||||||
|  | 	return fileicon.BasicThemeIcon(entry) | ||||||
|  | } | ||||||
|  |  | ||||||
| // RenderEmoji renders html text with emoji post processors | // RenderEmoji renders html text with emoji post processors | ||||||
| func (ut *RenderUtils) RenderEmoji(text string) template.HTML { | func (ut *RenderUtils) RenderEmoji(text string) template.HTML { | ||||||
| 	renderedText, err := markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), template.HTMLEscapeString(text)) | 	renderedText, err := markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), template.HTMLEscapeString(text)) | ||||||
|   | |||||||
| @@ -8,45 +8,46 @@ import ( | |||||||
| 	"html/template" | 	"html/template" | ||||||
|  |  | ||||||
| 	issues_model "code.gitea.io/gitea/models/issues" | 	issues_model "code.gitea.io/gitea/models/issues" | ||||||
|  | 	"code.gitea.io/gitea/modules/reqctx" | ||||||
| 	"code.gitea.io/gitea/modules/translation" | 	"code.gitea.io/gitea/modules/translation" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func renderEmojiLegacy(ctx context.Context, text string) template.HTML { | func renderEmojiLegacy(ctx context.Context, text string) template.HTML { | ||||||
| 	panicIfDevOrTesting() | 	panicIfDevOrTesting() | ||||||
| 	return NewRenderUtils(ctx).RenderEmoji(text) | 	return NewRenderUtils(reqctx.FromContext(ctx)).RenderEmoji(text) | ||||||
| } | } | ||||||
|  |  | ||||||
| func renderLabelLegacy(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML { | func renderLabelLegacy(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML { | ||||||
| 	panicIfDevOrTesting() | 	panicIfDevOrTesting() | ||||||
| 	return NewRenderUtils(ctx).RenderLabel(label) | 	return NewRenderUtils(reqctx.FromContext(ctx)).RenderLabel(label) | ||||||
| } | } | ||||||
|  |  | ||||||
| func renderLabelsLegacy(ctx context.Context, locale translation.Locale, labels []*issues_model.Label, repoLink string, issue *issues_model.Issue) template.HTML { | func renderLabelsLegacy(ctx context.Context, locale translation.Locale, labels []*issues_model.Label, repoLink string, issue *issues_model.Issue) template.HTML { | ||||||
| 	panicIfDevOrTesting() | 	panicIfDevOrTesting() | ||||||
| 	return NewRenderUtils(ctx).RenderLabels(labels, repoLink, issue) | 	return NewRenderUtils(reqctx.FromContext(ctx)).RenderLabels(labels, repoLink, issue) | ||||||
| } | } | ||||||
|  |  | ||||||
| func renderMarkdownToHtmlLegacy(ctx context.Context, input string) template.HTML { //nolint:revive | func renderMarkdownToHtmlLegacy(ctx context.Context, input string) template.HTML { //nolint:revive | ||||||
| 	panicIfDevOrTesting() | 	panicIfDevOrTesting() | ||||||
| 	return NewRenderUtils(ctx).MarkdownToHtml(input) | 	return NewRenderUtils(reqctx.FromContext(ctx)).MarkdownToHtml(input) | ||||||
| } | } | ||||||
|  |  | ||||||
| func renderCommitMessageLegacy(ctx context.Context, msg string, metas map[string]string) template.HTML { | func renderCommitMessageLegacy(ctx context.Context, msg string, metas map[string]string) template.HTML { | ||||||
| 	panicIfDevOrTesting() | 	panicIfDevOrTesting() | ||||||
| 	return NewRenderUtils(ctx).RenderCommitMessage(msg, metas) | 	return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitMessage(msg, metas) | ||||||
| } | } | ||||||
|  |  | ||||||
| func renderCommitMessageLinkSubjectLegacy(ctx context.Context, msg, urlDefault string, metas map[string]string) template.HTML { | func renderCommitMessageLinkSubjectLegacy(ctx context.Context, msg, urlDefault string, metas map[string]string) template.HTML { | ||||||
| 	panicIfDevOrTesting() | 	panicIfDevOrTesting() | ||||||
| 	return NewRenderUtils(ctx).RenderCommitMessageLinkSubject(msg, urlDefault, metas) | 	return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitMessageLinkSubject(msg, urlDefault, metas) | ||||||
| } | } | ||||||
|  |  | ||||||
| func renderIssueTitleLegacy(ctx context.Context, text string, metas map[string]string) template.HTML { | func renderIssueTitleLegacy(ctx context.Context, text string, metas map[string]string) template.HTML { | ||||||
| 	panicIfDevOrTesting() | 	panicIfDevOrTesting() | ||||||
| 	return NewRenderUtils(ctx).RenderIssueTitle(text, metas) | 	return NewRenderUtils(reqctx.FromContext(ctx)).RenderIssueTitle(text, metas) | ||||||
| } | } | ||||||
|  |  | ||||||
| func renderCommitBodyLegacy(ctx context.Context, msg string, metas map[string]string) template.HTML { | func renderCommitBodyLegacy(ctx context.Context, msg string, metas map[string]string) template.HTML { | ||||||
| 	panicIfDevOrTesting() | 	panicIfDevOrTesting() | ||||||
| 	return NewRenderUtils(ctx).RenderCommitBody(msg, metas) | 	return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitBody(msg, metas) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/markup" | 	"code.gitea.io/gitea/modules/markup" | ||||||
|  | 	"code.gitea.io/gitea/modules/reqctx" | ||||||
| 	"code.gitea.io/gitea/modules/test" | 	"code.gitea.io/gitea/modules/test" | ||||||
| 	"code.gitea.io/gitea/modules/translation" | 	"code.gitea.io/gitea/modules/translation" | ||||||
|  |  | ||||||
| @@ -67,9 +68,9 @@ func TestMain(m *testing.M) { | |||||||
| 	os.Exit(m.Run()) | 	os.Exit(m.Run()) | ||||||
| } | } | ||||||
|  |  | ||||||
| func newTestRenderUtils() *RenderUtils { | func newTestRenderUtils(t *testing.T) *RenderUtils { | ||||||
| 	ctx := context.Background() | 	ctx := reqctx.NewRequestContextForTest(t.Context()) | ||||||
| 	ctx = context.WithValue(ctx, translation.ContextKey, &translation.MockLocale{}) | 	ctx.SetContextValue(translation.ContextKey, &translation.MockLocale{}) | ||||||
| 	return NewRenderUtils(ctx) | 	return NewRenderUtils(ctx) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -105,7 +106,7 @@ func TestRenderCommitBody(t *testing.T) { | |||||||
| 			want: "second line", | 			want: "second line", | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 	ut := newTestRenderUtils() | 	ut := newTestRenderUtils(t) | ||||||
| 	for _, tt := range tests { | 	for _, tt := range tests { | ||||||
| 		t.Run(tt.name, func(t *testing.T) { | 		t.Run(tt.name, func(t *testing.T) { | ||||||
| 			assert.Equalf(t, tt.want, ut.RenderCommitBody(tt.args.msg, nil), "RenderCommitBody(%v, %v)", tt.args.msg, nil) | 			assert.Equalf(t, tt.want, ut.RenderCommitBody(tt.args.msg, nil), "RenderCommitBody(%v, %v)", tt.args.msg, nil) | ||||||
| @@ -131,17 +132,17 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit | |||||||
| <a href="/mention-user">@mention-user</a> test | <a href="/mention-user">@mention-user</a> test | ||||||
| <a href="/user13/repo11/issues/123" class="ref-issue">#123</a> | <a href="/user13/repo11/issues/123" class="ref-issue">#123</a> | ||||||
|   space` |   space` | ||||||
| 	assert.EqualValues(t, expected, string(newTestRenderUtils().RenderCommitBody(testInput(), testMetas))) | 	assert.EqualValues(t, expected, string(newTestRenderUtils(t).RenderCommitBody(testInput(), testMetas))) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestRenderCommitMessage(t *testing.T) { | func TestRenderCommitMessage(t *testing.T) { | ||||||
| 	expected := `space <a href="/mention-user" data-markdown-generated-content="">@mention-user</a>  ` | 	expected := `space <a href="/mention-user" data-markdown-generated-content="">@mention-user</a>  ` | ||||||
| 	assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitMessage(testInput(), testMetas)) | 	assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessage(testInput(), testMetas)) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestRenderCommitMessageLinkSubject(t *testing.T) { | func TestRenderCommitMessageLinkSubject(t *testing.T) { | ||||||
| 	expected := `<a href="https://example.com/link" class="muted">space </a><a href="/mention-user" data-markdown-generated-content="">@mention-user</a>` | 	expected := `<a href="https://example.com/link" class="muted">space </a><a href="/mention-user" data-markdown-generated-content="">@mention-user</a>` | ||||||
| 	assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", testMetas)) | 	assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", testMetas)) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestRenderIssueTitle(t *testing.T) { | func TestRenderIssueTitle(t *testing.T) { | ||||||
| @@ -168,7 +169,7 @@ mail@domain.com | |||||||
|   space<SPACE><SPACE> |   space<SPACE><SPACE> | ||||||
| ` | ` | ||||||
| 	expected = strings.ReplaceAll(expected, "<SPACE>", " ") | 	expected = strings.ReplaceAll(expected, "<SPACE>", " ") | ||||||
| 	assert.EqualValues(t, expected, string(newTestRenderUtils().RenderIssueTitle(testInput(), testMetas))) | 	assert.EqualValues(t, expected, string(newTestRenderUtils(t).RenderIssueTitle(testInput(), testMetas))) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestRenderMarkdownToHtml(t *testing.T) { | func TestRenderMarkdownToHtml(t *testing.T) { | ||||||
| @@ -194,11 +195,11 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit | |||||||
| #123 | #123 | ||||||
| space</p> | space</p> | ||||||
| ` | ` | ||||||
| 	assert.Equal(t, expected, string(newTestRenderUtils().MarkdownToHtml(testInput()))) | 	assert.Equal(t, expected, string(newTestRenderUtils(t).MarkdownToHtml(testInput()))) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestRenderLabels(t *testing.T) { | func TestRenderLabels(t *testing.T) { | ||||||
| 	ut := newTestRenderUtils() | 	ut := newTestRenderUtils(t) | ||||||
| 	label := &issues.Label{ID: 123, Name: "label-name", Color: "label-color"} | 	label := &issues.Label{ID: 123, Name: "label-name", Color: "label-color"} | ||||||
| 	issue := &issues.Issue{} | 	issue := &issues.Issue{} | ||||||
| 	expected := `/owner/repo/issues?labels=123` | 	expected := `/owner/repo/issues?labels=123` | ||||||
| @@ -212,6 +213,6 @@ func TestRenderLabels(t *testing.T) { | |||||||
|  |  | ||||||
| func TestUserMention(t *testing.T) { | func TestUserMention(t *testing.T) { | ||||||
| 	markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true | 	markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true | ||||||
| 	rendered := newTestRenderUtils().MarkdownToHtml("@no-such-user @mention-user @mention-user") | 	rendered := newTestRenderUtils(t).MarkdownToHtml("@no-such-user @mention-user @mention-user") | ||||||
| 	assert.EqualValues(t, `<p>@no-such-user <a href="/mention-user" rel="nofollow">@mention-user</a> <a href="/mention-user" rel="nofollow">@mention-user</a></p>`, strings.TrimSpace(string(rendered))) | 	assert.EqualValues(t, `<p>@no-such-user <a href="/mention-user" rel="nofollow">@mention-user</a> <a href="/mention-user" rel="nofollow">@mention-user</a></p>`, strings.TrimSpace(string(rendered))) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										12541
									
								
								options/fileicon/material-icon-rules.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12541
									
								
								options/fileicon/material-icon-rules.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1074
									
								
								options/fileicon/material-icon-svgs.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1074
									
								
								options/fileicon/material-icon-svgs.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										110
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										110
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -100,6 +100,7 @@ | |||||||
|         "eslint-plugin-wc": "2.2.0", |         "eslint-plugin-wc": "2.2.0", | ||||||
|         "happy-dom": "17.1.0", |         "happy-dom": "17.1.0", | ||||||
|         "markdownlint-cli": "0.44.0", |         "markdownlint-cli": "0.44.0", | ||||||
|  |         "material-icon-theme": "5.20.0", | ||||||
|         "nolyfill": "1.0.43", |         "nolyfill": "1.0.43", | ||||||
|         "postcss-html": "1.8.0", |         "postcss-html": "1.8.0", | ||||||
|         "stylelint": "16.14.1", |         "stylelint": "16.14.1", | ||||||
| @@ -4675,6 +4676,13 @@ | |||||||
|         "node": ">= 6" |         "node": ">= 6" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/chroma-js": { | ||||||
|  |       "version": "3.1.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-3.1.2.tgz", | ||||||
|  |       "integrity": "sha512-IJnETTalXbsLx1eKEgx19d5L6SRM7cH4vINw/99p/M11HCuXGRWL+6YmCm7FWFGIo6dtWuQoQi1dc5yQ7ESIHg==", | ||||||
|  |       "dev": true, | ||||||
|  |       "license": "(BSD-3-Clause AND Apache-2.0)" | ||||||
|  |     }, | ||||||
|     "node_modules/chrome-trace-event": { |     "node_modules/chrome-trace-event": { | ||||||
|       "version": "1.0.4", |       "version": "1.0.4", | ||||||
|       "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", |       "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", | ||||||
| @@ -5702,6 +5710,33 @@ | |||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/deep-rename-keys": { | ||||||
|  |       "version": "0.2.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/deep-rename-keys/-/deep-rename-keys-0.2.1.tgz", | ||||||
|  |       "integrity": "sha512-RHd9ABw4Fvk+gYDWqwOftG849x0bYOySl/RgX0tLI9i27ZIeSO91mLZJEp7oPHOMFqHvpgu21YptmDt0FYD/0A==", | ||||||
|  |       "dev": true, | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "kind-of": "^3.0.2", | ||||||
|  |         "rename-keys": "^1.1.2" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=0.10.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/deep-rename-keys/node_modules/kind-of": { | ||||||
|  |       "version": "3.2.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", | ||||||
|  |       "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", | ||||||
|  |       "dev": true, | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "is-buffer": "^1.1.5" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=0.10.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/delaunator": { |     "node_modules/delaunator": { | ||||||
|       "version": "5.0.1", |       "version": "5.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", |       "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", | ||||||
| @@ -7004,6 +7039,13 @@ | |||||||
|         "node": ">=6" |         "node": ">=6" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/eventemitter3": { | ||||||
|  |       "version": "2.0.3", | ||||||
|  |       "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", | ||||||
|  |       "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==", | ||||||
|  |       "dev": true, | ||||||
|  |       "license": "MIT" | ||||||
|  |     }, | ||||||
|     "node_modules/events": { |     "node_modules/events": { | ||||||
|       "version": "3.3.0", |       "version": "3.3.0", | ||||||
|       "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", |       "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", | ||||||
| @@ -7856,6 +7898,13 @@ | |||||||
|         "node": ">=8" |         "node": ">=8" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/is-buffer": { | ||||||
|  |       "version": "1.1.6", | ||||||
|  |       "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", | ||||||
|  |       "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", | ||||||
|  |       "dev": true, | ||||||
|  |       "license": "MIT" | ||||||
|  |     }, | ||||||
|     "node_modules/is-builtin-module": { |     "node_modules/is-builtin-module": { | ||||||
|       "version": "3.2.1", |       "version": "3.2.1", | ||||||
|       "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", |       "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", | ||||||
| @@ -8840,6 +8889,25 @@ | |||||||
|         "node": ">= 12" |         "node": ">= 12" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/material-icon-theme": { | ||||||
|  |       "version": "5.20.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/material-icon-theme/-/material-icon-theme-5.20.0.tgz", | ||||||
|  |       "integrity": "sha512-EAz5I2O7Hq6G8Rv0JdO6NXL+jK/mvDppcVUVbsUMpSqSmFczNdaR5WJ3lOiRz4HNBlEN2i2sVSfuqI5iNQfGLg==", | ||||||
|  |       "dev": true, | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "chroma-js": "^3.0.0", | ||||||
|  |         "events": "^3.3.0", | ||||||
|  |         "fast-deep-equal": "^3.1.3", | ||||||
|  |         "svgson": "^5.3.1" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "vscode": "^1.55.0" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://github.com/sponsors/material-extensions" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/mathml-tag-names": { |     "node_modules/mathml-tag-names": { | ||||||
|       "version": "2.1.3", |       "version": "2.1.3", | ||||||
|       "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", |       "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", | ||||||
| @@ -11020,6 +11088,16 @@ | |||||||
|         "jsesc": "bin/jsesc" |         "jsesc": "bin/jsesc" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/rename-keys": { | ||||||
|  |       "version": "1.2.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/rename-keys/-/rename-keys-1.2.0.tgz", | ||||||
|  |       "integrity": "sha512-U7XpAktpbSgHTRSNRrjKSrjYkZKuhUukfoBlXWXUExCAqhzh1TU3BDRAfJmarcl5voKS+pbKU9MvyLWKZ4UEEg==", | ||||||
|  |       "dev": true, | ||||||
|  |       "license": "MIT", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">= 0.8.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/require-directory": { |     "node_modules/require-directory": { | ||||||
|       "version": "2.1.1", |       "version": "2.1.1", | ||||||
|       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", |       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", | ||||||
| @@ -12258,6 +12336,17 @@ | |||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "CC0-1.0" |       "license": "CC0-1.0" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/svgson": { | ||||||
|  |       "version": "5.3.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/svgson/-/svgson-5.3.1.tgz", | ||||||
|  |       "integrity": "sha512-qdPgvUNWb40gWktBJnbJRelWcPzkLed/ShhnRsjbayXz8OtdPOzbil9jtiZdrYvSDumAz/VNQr6JaNfPx/gvPA==", | ||||||
|  |       "dev": true, | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "deep-rename-keys": "^0.2.1", | ||||||
|  |         "xml-reader": "2.4.3" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/swagger-ui-dist": { |     "node_modules/swagger-ui-dist": { | ||||||
|       "version": "5.18.3", |       "version": "5.18.3", | ||||||
|       "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.3.tgz", |       "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.3.tgz", | ||||||
| @@ -14107,6 +14196,16 @@ | |||||||
|         "node": "^14.17.0 || ^16.13.0 || >=18.0.0" |         "node": "^14.17.0 || ^16.13.0 || >=18.0.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/xml-lexer": { | ||||||
|  |       "version": "0.2.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/xml-lexer/-/xml-lexer-0.2.2.tgz", | ||||||
|  |       "integrity": "sha512-G0i98epIwiUEiKmMcavmVdhtymW+pCAohMRgybyIME9ygfVu8QheIi+YoQh3ngiThsT0SQzJT4R0sKDEv8Ou0w==", | ||||||
|  |       "dev": true, | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "eventemitter3": "^2.0.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/xml-name-validator": { |     "node_modules/xml-name-validator": { | ||||||
|       "version": "4.0.0", |       "version": "4.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", |       "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", | ||||||
| @@ -14117,6 +14216,17 @@ | |||||||
|         "node": ">=12" |         "node": ">=12" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/xml-reader": { | ||||||
|  |       "version": "2.4.3", | ||||||
|  |       "resolved": "https://registry.npmjs.org/xml-reader/-/xml-reader-2.4.3.tgz", | ||||||
|  |       "integrity": "sha512-xWldrIxjeAMAu6+HSf9t50ot1uL5M+BtOidRCWHXIeewvSeIpscWCsp4Zxjk8kHHhdqFBrfK8U0EJeCcnyQ/gA==", | ||||||
|  |       "dev": true, | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "eventemitter3": "^2.0.0", | ||||||
|  |         "xml-lexer": "^0.2.2" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/y18n": { |     "node_modules/y18n": { | ||||||
|       "version": "5.0.8", |       "version": "5.0.8", | ||||||
|       "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", |       "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", | ||||||
|   | |||||||
| @@ -99,6 +99,7 @@ | |||||||
|     "eslint-plugin-wc": "2.2.0", |     "eslint-plugin-wc": "2.2.0", | ||||||
|     "happy-dom": "17.1.0", |     "happy-dom": "17.1.0", | ||||||
|     "markdownlint-cli": "0.44.0", |     "markdownlint-cli": "0.44.0", | ||||||
|  |     "material-icon-theme": "5.20.0", | ||||||
|     "nolyfill": "1.0.43", |     "nolyfill": "1.0.43", | ||||||
|     "postcss-html": "1.8.0", |     "postcss-html": "1.8.0", | ||||||
|     "stylelint": "16.14.1", |     "stylelint": "16.14.1", | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								public/assets/img/svg/material-folder-generic.svg
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/assets/img/svg/material-folder-generic.svg
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="svg material-folder-generic" width="16" height="16" aria-hidden="true"><path fill="#42a5f5" d="M10 4H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8z"/></svg> | ||||||
| After Width: | Height: | Size: 250 B | 
							
								
								
									
										1
									
								
								public/assets/img/svg/material-folder-symlink.svg
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/assets/img/svg/material-folder-symlink.svg
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="svg material-folder-symlink" width="16" height="16" aria-hidden="true"><path fill="#42a5f5" d="M10 4H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8z" opacity=".745"/><path fill="#c5e5fd" d="M16.972 10.757v2.641h-6.561v5.281h6.561v2.641l6.562-5.281z" opacity=".81"/></svg> | ||||||
| After Width: | Height: | Size: 364 B | 
| @@ -15,8 +15,8 @@ | |||||||
| 			{{$commit := $item.Commit}} | 			{{$commit := $item.Commit}} | ||||||
| 			{{$submoduleFile := $item.SubmoduleFile}} | 			{{$submoduleFile := $item.SubmoduleFile}} | ||||||
| 			<div class="repo-file-cell name {{if not $commit}}notready{{end}}"> | 			<div class="repo-file-cell name {{if not $commit}}notready{{end}}"> | ||||||
|  | 				{{ctx.RenderUtils.RenderFileIcon $entry}} | ||||||
| 				{{if $entry.IsSubModule}} | 				{{if $entry.IsSubModule}} | ||||||
| 					{{svg "octicon-file-submodule"}} |  | ||||||
| 					{{$submoduleLink := $submoduleFile.SubmoduleWebLink ctx}} | 					{{$submoduleLink := $submoduleFile.SubmoduleWebLink ctx}} | ||||||
| 					{{if $submoduleLink}} | 					{{if $submoduleLink}} | ||||||
| 						<a class="muted" href="{{$submoduleLink.RepoWebLink}}">{{$entry.Name}}</a> <span class="at">@</span> <a href="{{$submoduleLink.CommitWebLink}}">{{ShortSha $submoduleFile.RefID}}</a> | 						<a class="muted" href="{{$submoduleLink.RepoWebLink}}">{{$entry.Name}}</a> <span class="at">@</span> <a href="{{$submoduleLink.CommitWebLink}}">{{ShortSha $submoduleFile.RefID}}</a> | ||||||
| @@ -26,7 +26,6 @@ | |||||||
| 				{{else}} | 				{{else}} | ||||||
| 					{{if $entry.IsDir}} | 					{{if $entry.IsDir}} | ||||||
| 						{{$subJumpablePathName := $entry.GetSubJumpablePathName}} | 						{{$subJumpablePathName := $entry.GetSubJumpablePathName}} | ||||||
| 						{{svg "octicon-file-directory-fill"}} |  | ||||||
| 						<a class="muted" href="{{$.TreeLink}}/{{PathEscapeSegments $subJumpablePathName}}" title="{{$subJumpablePathName}}"> | 						<a class="muted" href="{{$.TreeLink}}/{{PathEscapeSegments $subJumpablePathName}}" title="{{$subJumpablePathName}}"> | ||||||
| 							{{$subJumpablePathFields := StringUtils.Split $subJumpablePathName "/"}} | 							{{$subJumpablePathFields := StringUtils.Split $subJumpablePathName "/"}} | ||||||
| 							{{$subJumpablePathFieldLast := (Eval (len $subJumpablePathFields) "-" 1)}} | 							{{$subJumpablePathFieldLast := (Eval (len $subJumpablePathFields) "-" 1)}} | ||||||
| @@ -38,7 +37,6 @@ | |||||||
| 							{{end}} | 							{{end}} | ||||||
| 						</a> | 						</a> | ||||||
| 					{{else}} | 					{{else}} | ||||||
| 						{{svg (printf "octicon-%s" (EntryIcon $entry))}} |  | ||||||
| 						<a class="muted" href="{{$.TreeLink}}/{{PathEscapeSegments $entry.Name}}" title="{{$entry.Name}}">{{$entry.Name}}</a> | 						<a class="muted" href="{{$.TreeLink}}/{{PathEscapeSegments $entry.Name}}" title="{{$entry.Name}}">{{$entry.Name}}</a> | ||||||
| 					{{end}} | 					{{end}} | ||||||
| 				{{end}} | 				{{end}} | ||||||
|   | |||||||
| @@ -164,7 +164,7 @@ func TestViewRepo1CloneLinkAuthorized(t *testing.T) { | |||||||
|  |  | ||||||
| func TestViewRepoWithSymlinks(t *testing.T) { | func TestViewRepoWithSymlinks(t *testing.T) { | ||||||
| 	defer tests.PrepareTestEnv(t)() | 	defer tests.PrepareTestEnv(t)() | ||||||
|  | 	defer test.MockVariableValue(&setting.UI.FileIconTheme, "basic")() | ||||||
| 	session := loginUser(t, "user2") | 	session := loginUser(t, "user2") | ||||||
|  |  | ||||||
| 	req := NewRequest(t, "GET", "/user2/repo20.git") | 	req := NewRequest(t, "GET", "/user2/repo20.git") | ||||||
|   | |||||||
| @@ -5,27 +5,20 @@ import {parse} from 'node:path'; | |||||||
| import {readFile, writeFile, mkdir} from 'node:fs/promises'; | import {readFile, writeFile, mkdir} from 'node:fs/promises'; | ||||||
| import {fileURLToPath} from 'node:url'; | import {fileURLToPath} from 'node:url'; | ||||||
| import {exit} from 'node:process'; | import {exit} from 'node:process'; | ||||||
|  | import * as fs from 'node:fs'; | ||||||
|  |  | ||||||
| const glob = (pattern) => fastGlob.sync(pattern, { | const glob = (pattern) => fastGlob.sync(pattern, { | ||||||
|   cwd: fileURLToPath(new URL('..', import.meta.url)), |   cwd: fileURLToPath(new URL('..', import.meta.url)), | ||||||
|   absolute: true, |   absolute: true, | ||||||
| }); | }); | ||||||
|  |  | ||||||
| function doExit(err) { | async function processAssetsSvgFile(file, {prefix, fullName} = {}) { | ||||||
|   if (err) console.error(err); |   let name = fullName; | ||||||
|   exit(err ? 1 : 0); |   if (!name) { | ||||||
| } |  | ||||||
|  |  | ||||||
| async function processFile(file, {prefix, fullName} = {}) { |  | ||||||
|   let name; |  | ||||||
|   if (fullName) { |  | ||||||
|     name = fullName; |  | ||||||
|   } else { |  | ||||||
|     name = parse(file).name; |     name = parse(file).name; | ||||||
|     if (prefix) name = `${prefix}-${name}`; |     if (prefix) name = `${prefix}-${name}`; | ||||||
|     if (prefix === 'octicon') name = name.replace(/-[0-9]+$/, ''); // chop of '-16' on octicons |     if (prefix === 'octicon') name = name.replace(/-[0-9]+$/, ''); // chop of '-16' on octicons | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Set the `xmlns` attribute so that the files are displayable in standalone documents |   // Set the `xmlns` attribute so that the files are displayable in standalone documents | ||||||
|   // The svg backend module will strip the attribute during startup for inline display |   // The svg backend module will strip the attribute during startup for inline display | ||||||
|   const {data} = optimize(await readFile(file, 'utf8'), { |   const {data} = optimize(await readFile(file, 'utf8'), { | ||||||
| @@ -44,28 +37,50 @@ async function processFile(file, {prefix, fullName} = {}) { | |||||||
|       }, |       }, | ||||||
|     ], |     ], | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   await writeFile(fileURLToPath(new URL(`../public/assets/img/svg/${name}.svg`, import.meta.url)), data); |   await writeFile(fileURLToPath(new URL(`../public/assets/img/svg/${name}.svg`, import.meta.url)), data); | ||||||
| } | } | ||||||
|  |  | ||||||
| function processFiles(pattern, opts) { | function processAssetsSvgFiles(pattern, opts) { | ||||||
|   return glob(pattern).map((file) => processFile(file, opts)); |   return glob(pattern).map((file) => processAssetsSvgFile(file, opts)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function processMaterialFileIcons() { | ||||||
|  |   const files = glob('node_modules/material-icon-theme/icons/*.svg'); | ||||||
|  |   const svgSymbols = {}; | ||||||
|  |   for (const file of files) { | ||||||
|  |     // remove all unnecessary attributes, only keep "viewBox" | ||||||
|  |     const {data} = optimize(await readFile(file, 'utf8'), { | ||||||
|  |       plugins: [ | ||||||
|  |         {name: 'preset-default'}, | ||||||
|  |         {name: 'removeDimensions'}, | ||||||
|  |         {name: 'removeXMLNS'}, | ||||||
|  |         {name: 'removeAttrs', params: {attrs: 'xml:space', elemSeparator: ','}}, | ||||||
|  |       ], | ||||||
|  |     }); | ||||||
|  |     const svgName = parse(file).name; | ||||||
|  |     // intentionally use single quote here to avoid escaping | ||||||
|  |     svgSymbols[svgName] = data.replace(/"/g, `'`); | ||||||
|  |   } | ||||||
|  |   fs.writeFileSync(fileURLToPath(new URL(`../options/fileicon/material-icon-svgs.json`, import.meta.url)), JSON.stringify(svgSymbols, null, 2)); | ||||||
|  |  | ||||||
|  |   const iconRules = await readFile(fileURLToPath(new URL(`../node_modules/material-icon-theme/dist/material-icons.json`, import.meta.url))); | ||||||
|  |   const iconRulesPretty = JSON.stringify(JSON.parse(iconRules), null, 2); | ||||||
|  |   fs.writeFileSync(fileURLToPath(new URL(`../options/fileicon/material-icon-rules.json`, import.meta.url)), iconRulesPretty); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function main() { | async function main() { | ||||||
|   try { |   await mkdir(fileURLToPath(new URL('../public/assets/img/svg', import.meta.url)), {recursive: true}); | ||||||
|     await mkdir(fileURLToPath(new URL('../public/assets/img/svg', import.meta.url)), {recursive: true}); |  | ||||||
|   } catch {} |  | ||||||
|  |  | ||||||
|   await Promise.all([ |   await Promise.all([ | ||||||
|     ...processFiles('node_modules/@primer/octicons/build/svg/*-16.svg', {prefix: 'octicon'}), |     ...processAssetsSvgFiles('node_modules/@primer/octicons/build/svg/*-16.svg', {prefix: 'octicon'}), | ||||||
|     ...processFiles('web_src/svg/*.svg'), |     ...processAssetsSvgFiles('web_src/svg/*.svg'), | ||||||
|     ...processFiles('public/assets/img/gitea.svg', {fullName: 'gitea-gitea'}), |     ...processAssetsSvgFiles('public/assets/img/gitea.svg', {fullName: 'gitea-gitea'}), | ||||||
|  |     processMaterialFileIcons(), | ||||||
|   ]); |   ]); | ||||||
| } | } | ||||||
|  |  | ||||||
| try { | try { | ||||||
|   doExit(await main()); |   await main(); | ||||||
| } catch (err) { | } catch (err) { | ||||||
|   doExit(err); |   console.error(err); | ||||||
|  |   exit(1); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,6 +4,10 @@ | |||||||
|   fill: currentcolor; |   fill: currentcolor; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .svg.fileicon { | ||||||
|  |   fill: transparent; /* some material icons have dark background fill, so need to reset */ | ||||||
|  | } | ||||||
|  |  | ||||||
| .middle .svg { | .middle .svg { | ||||||
|   vertical-align: middle; |   vertical-align: middle; | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								web_src/svg/material-folder-generic.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								web_src/svg/material-folder-generic.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 4H4c-1.11 0-2 .89-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z" fill="#42a5f5"/></svg> | ||||||
| After Width: | Height: | Size: 195 B | 
							
								
								
									
										1
									
								
								web_src/svg/material-folder-symlink.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								web_src/svg/material-folder-symlink.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 4H4c-1.11 0-2 .89-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z" fill="#42a5f5" opacity=".745"/><path d="M16.972 10.757v2.641h-6.561v5.281h6.561v2.641l6.562-5.281-6.562-5.282z" opacity=".81" fill="#c5e5fd"/></svg> | ||||||
| After Width: | Height: | Size: 321 B | 
		Reference in New Issue
	
	Block a user