mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
163 lines
4.4 KiB
Go
163 lines
4.4 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package fileicon
|
|
|
|
import (
|
|
"html/template"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/hanzoai/git/modules/json"
|
|
"github.com/hanzoai/git/modules/log"
|
|
"github.com/hanzoai/git/modules/options"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
"github.com/hanzoai/git/modules/svg"
|
|
"github.com/hanzoai/git/modules/util"
|
|
)
|
|
|
|
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 href="#` + svgID + `"></use></svg>`)
|
|
}
|
|
|
|
func (m *MaterialIconProvider) EntryIconHTML(p *RenderedIconPool, entry *EntryInfo) template.HTML {
|
|
if m.rules == nil {
|
|
return BasicEntryIconHTML(entry)
|
|
}
|
|
|
|
if entry.EntryMode.IsLink() {
|
|
if entry.SymlinkToMode.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
|
|
}
|
|
|
|
name := m.FindIconName(entry)
|
|
iconSVG := m.svgs[name]
|
|
if iconSVG == "" {
|
|
name = "file"
|
|
if entry.EntryMode.IsDir() {
|
|
name = util.Iif(entry.IsOpen, "folder-open", "folder")
|
|
}
|
|
iconSVG = m.svgs[name]
|
|
if iconSVG == "" {
|
|
setting.PanicInDevOrTesting("missing file icon for %s", name)
|
|
}
|
|
}
|
|
|
|
// 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 = BasicEntryIconName(entry)
|
|
case entry.EntryMode.IsSubModule():
|
|
extraClass = "octicon-file-submodule"
|
|
}
|
|
return m.renderFileIconSVG(p, name, iconSVG, extraClass)
|
|
}
|
|
|
|
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(entry *EntryInfo) string {
|
|
if entry.EntryMode.IsSubModule() {
|
|
return "folder-git"
|
|
}
|
|
|
|
fileNameLower := strings.ToLower(entry.BaseName)
|
|
if entry.EntryMode.IsDir() {
|
|
if s, ok := m.rules.FolderNames[fileNameLower]; ok {
|
|
return s
|
|
}
|
|
return util.Iif(entry.IsOpen, "folder-open", "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"
|
|
}
|