aboutsummaryrefslogtreecommitdiffstats
path: root/modules/fileicon/material.go
blob: 557f7ca9e47cb48aa69d4bd59d1bb319e5803e9c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// 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/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"`
	if p.IconSVGs[svgID] == "" {
		p.IconSVGs[svgID] = template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:])
	}
	return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
}

func (m *MaterialIconProvider) FileIcon(p *RenderedIconPool, entry *git.TreeEntry) template.HTML {
	if m.rules == nil {
		return BasicThemeIcon(entry)
	}

	if entry.IsLink() {
		if te, err := entry.FollowLink(); err == nil && te.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.findIconNameByGit(entry)
	// 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 && name != "folder" && iconSVG != "" {
		// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
		extraClass := "octicon-file"
		switch {
		case entry.IsDir():
			extraClass = "octicon-file-directory-fill"
		case entry.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 BasicThemeIcon(entry)
}

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 bool) string {
	fileNameLower := strings.ToLower(path.Base(name))
	if isDir {
		if s, ok := m.rules.FolderNames[fileNameLower]; ok {
			return s
		}
		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 *git.TreeEntry) string {
	if entry.IsSubModule() {
		return "folder-git"
	}
	return m.FindIconName(entry.Name(), entry.IsDir())
}