aboutsummaryrefslogtreecommitdiffstats
path: root/modules/fileicon/material.go
blob: 5361592d8a30ded7d97dc2a6867bb13351c0724d (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
156
157
158
159
160
161
162
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package fileicon

import (
	"html/template"
	"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/setting"
	"code.gitea.io/gitea/modules/svg"
	"code.gitea.io/gitea/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 xlink: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"
}