1
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-04-18 00:47:48 -04:00
gitea/modules/fileicon/material.go
Kerwin Bryant ec3dcafddd Extract the parameter of RenderEntryIconWithOpenStatus from git.TreeEntry to FileEntry to decouple it from git.TreeEntry.
This change will allow integrating the file icons of the file tree on the diff page into the unified rendering logic without relying on `git.TreeEntry`.
2025-04-10 12:21:50 +00:00

168 lines
4.9 KiB
Go

// 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/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/options"
"code.gitea.io/gitea/modules/svg"
)
type materialIconRulesData struct {
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 {
materialIconProvider.once.Do(materialIconProvider.loadData)
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(p *RenderedIconPool, name, svg, extraClass string) template.HTML {
// 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 git-entry-icon ` + extraClass + `" width="16" height="16" aria-hidden="true"`
svgHTML := template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:])
if p == nil {
return svgHTML
}
if p.IconSVGs[svgID] == "" {
p.IconSVGs[svgID] = svgHTML
}
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
}
func (m *MaterialIconProvider) FolderIconWithOpenStatus(p *RenderedIconPool, isOpen bool) template.HTML {
name := m.FindIconName("folder", true, isOpen)
return m.renderFileIconSVG(p, name, m.svgs[name], BasicThemeFolderIconName(isOpen))
}
func (m *MaterialIconProvider) FileIconWithOpenStatus(p *RenderedIconPool, entry *FileEntry, isOpen bool) template.HTML {
if m.rules == nil {
return BasicThemeIconWithOpenStatus(entry, isOpen)
}
if entry.EntryMode.IsLink() {
if entry.FollowEntryMode.IsDir() {
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
return svg.RenderHTML("material-folder-symlink", 16, "octicon-file-directory-symlink")
}
return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them
}
// TODO: add "open icon" support
name := m.findIconNameByGit(entry, isOpen)
// the material icon pack's "folder" icon doesn't look good, so use our built-in one
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
if iconSVG, ok := m.svgs[name]; ok && iconSVG != "" {
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
extraClass := "octicon-file"
switch {
case entry.EntryMode.IsDir():
extraClass = BasicThemeFolderIconName(isOpen)
case entry.EntryMode.IsSubModule():
extraClass = "octicon-file-submodule"
}
return m.renderFileIconSVG(p, name, iconSVG, extraClass)
}
// TODO: use an interface or wrapper for git.Entry to make the code testable.
return BasicThemeIconWithOpenStatus(entry, isOpen)
}
func (m *MaterialIconProvider) findIconNameWithLangID(s string) string {
if _, ok := m.svgs[s]; ok {
return s
}
if s, ok := m.rules.LanguageIDs[s]; ok {
if _, ok = m.svgs[s]; ok {
return s
}
}
return ""
}
func (m *MaterialIconProvider) FindIconName(name string, isDir, isOpen bool) string {
fileNameLower := strings.ToLower(path.Base(name))
if isDir {
if s, ok := m.rules.FolderNames[fileNameLower]; ok {
return s
}
if isOpen {
return "folder-open"
}
return "folder"
}
if s, ok := m.rules.FileNames[fileNameLower]; ok {
if s = m.findIconNameWithLangID(s); s != "" {
return s
}
}
for i := len(fileNameLower) - 1; i >= 0; i-- {
if fileNameLower[i] == '.' {
ext := fileNameLower[i+1:]
if s, ok := m.rules.FileExtensions[ext]; ok {
if s = m.findIconNameWithLangID(s); s != "" {
return s
}
}
}
}
return "file"
}
func (m *MaterialIconProvider) findIconNameByGit(entry *FileEntry, isOpen bool) string {
if entry.EntryMode.IsSubModule() {
return "folder-git"
}
return m.FindIconName(entry.Name, entry.EntryMode.IsDir(), isOpen)
}