4
4
package webtheme
5
5
6
6
import (
7
+ "regexp"
7
8
"sort"
8
9
"strings"
9
10
"sync"
@@ -12,63 +13,154 @@ import (
12
13
"code.gitea.io/gitea/modules/log"
13
14
"code.gitea.io/gitea/modules/public"
14
15
"code.gitea.io/gitea/modules/setting"
16
+ "code.gitea.io/gitea/modules/util"
15
17
)
16
18
17
19
var (
18
- availableThemes [] string
19
- availableThemesSet container.Set [string ]
20
- themeOnce sync.Once
20
+ availableThemes [] * ThemeMetaInfo
21
+ availableThemeInternalNames container.Set [string ]
22
+ themeOnce sync.Once
21
23
)
22
24
25
+ const (
26
+ fileNamePrefix = "theme-"
27
+ fileNameSuffix = ".css"
28
+ )
29
+
30
+ type ThemeMetaInfo struct {
31
+ FileName string
32
+ InternalName string
33
+ DisplayName string
34
+ }
35
+
36
+ func parseThemeMetaInfoToMap (cssContent string ) map [string ]string {
37
+ /*
38
+ The theme meta info is stored in the CSS file's variables of `gitea-theme-meta-info` element,
39
+ which is a privately defined and is only used by backend to extract the meta info.
40
+ Not using ":root" because it is difficult to parse various ":root" blocks when importing other files,
41
+ it is difficult to control the overriding, and it's difficult to avoid user's customized overridden styles.
42
+ */
43
+ metaInfoContent := cssContent
44
+ if pos := strings .LastIndex (metaInfoContent , "gitea-theme-meta-info" ); pos >= 0 {
45
+ metaInfoContent = metaInfoContent [pos :]
46
+ }
47
+
48
+ reMetaInfoItem := `
49
+ (
50
+ \s*(--[-\w]+)
51
+ \s*:
52
+ \s*(
53
+ ("(\\"|[^"])*")
54
+ |('(\\'|[^'])*')
55
+ |([^'";]+)
56
+ )
57
+ \s*;
58
+ \s*
59
+ )
60
+ `
61
+ reMetaInfoItem = strings .ReplaceAll (reMetaInfoItem , "\n " , "" )
62
+ reMetaInfoBlock := `\bgitea-theme-meta-info\s*\{(` + reMetaInfoItem + `+)\}`
63
+ re := regexp .MustCompile (reMetaInfoBlock )
64
+ matchedMetaInfoBlock := re .FindAllStringSubmatch (metaInfoContent , - 1 )
65
+ if len (matchedMetaInfoBlock ) == 0 {
66
+ return nil
67
+ }
68
+ re = regexp .MustCompile (strings .ReplaceAll (reMetaInfoItem , "\n " , "" ))
69
+ matchedItems := re .FindAllStringSubmatch (matchedMetaInfoBlock [0 ][1 ], - 1 )
70
+ m := map [string ]string {}
71
+ for _ , item := range matchedItems {
72
+ v := item [3 ]
73
+ if strings .HasPrefix (v , `"` ) {
74
+ v = strings .TrimSuffix (strings .TrimPrefix (v , `"` ), `"` )
75
+ v = strings .ReplaceAll (v , `\"` , `"` )
76
+ } else if strings .HasPrefix (v , `'` ) {
77
+ v = strings .TrimSuffix (strings .TrimPrefix (v , `'` ), `'` )
78
+ v = strings .ReplaceAll (v , `\'` , `'` )
79
+ }
80
+ m [item [2 ]] = v
81
+ }
82
+ return m
83
+ }
84
+
85
+ func defaultThemeMetaInfoByFileName (fileName string ) * ThemeMetaInfo {
86
+ themeInfo := & ThemeMetaInfo {
87
+ FileName : fileName ,
88
+ InternalName : strings .TrimSuffix (strings .TrimPrefix (fileName , fileNamePrefix ), fileNameSuffix ),
89
+ }
90
+ themeInfo .DisplayName = themeInfo .InternalName
91
+ return themeInfo
92
+ }
93
+
94
+ func defaultThemeMetaInfoByInternalName (fileName string ) * ThemeMetaInfo {
95
+ return defaultThemeMetaInfoByFileName (fileNamePrefix + fileName + fileNameSuffix )
96
+ }
97
+
98
+ func parseThemeMetaInfo (fileName , cssContent string ) * ThemeMetaInfo {
99
+ themeInfo := defaultThemeMetaInfoByFileName (fileName )
100
+ m := parseThemeMetaInfoToMap (cssContent )
101
+ if m == nil {
102
+ return themeInfo
103
+ }
104
+ themeInfo .DisplayName = m ["--theme-display-name" ]
105
+ return themeInfo
106
+ }
107
+
23
108
func initThemes () {
24
109
availableThemes = nil
25
110
defer func () {
26
- availableThemesSet = container .SetOf (availableThemes ... )
27
- if ! availableThemesSet .Contains (setting .UI .DefaultTheme ) {
111
+ availableThemeInternalNames = container.Set [string ]{}
112
+ for _ , theme := range availableThemes {
113
+ availableThemeInternalNames .Add (theme .InternalName )
114
+ }
115
+ if ! availableThemeInternalNames .Contains (setting .UI .DefaultTheme ) {
28
116
setting .LogStartupProblem (1 , log .ERROR , "Default theme %q is not available, please correct the '[ui].DEFAULT_THEME' setting in the config file" , setting .UI .DefaultTheme )
29
117
}
30
118
}()
31
119
cssFiles , err := public .AssetFS ().ListFiles ("/assets/css" )
32
120
if err != nil {
33
121
log .Error ("Failed to list themes: %v" , err )
34
- availableThemes = []string { setting .UI .DefaultTheme }
122
+ availableThemes = []* ThemeMetaInfo { defaultThemeMetaInfoByInternalName ( setting .UI .DefaultTheme ) }
35
123
return
36
124
}
37
- var foundThemes []string
38
- for _ , name := range cssFiles {
39
- name , ok := strings .CutPrefix ( name , "theme-" )
40
- if ! ok {
41
- continue
42
- }
43
- name , ok = strings . CutSuffix ( name , ".css" )
44
- if ! ok {
45
- continue
125
+ var foundThemes []* ThemeMetaInfo
126
+ for _ , fileName := range cssFiles {
127
+ if strings . HasPrefix ( fileName , fileNamePrefix ) && strings .HasSuffix ( fileName , fileNameSuffix ) {
128
+ content , err := public . AssetFS (). ReadFile ( "/assets/css/" + fileName )
129
+ if err != nil {
130
+ log . Error ( "Failed to read theme file %q: %v" , fileName , err )
131
+ continue
132
+ }
133
+ foundThemes = append ( foundThemes , parseThemeMetaInfo ( fileName , util . UnsafeBytesToString ( content )))
46
134
}
47
- foundThemes = append (foundThemes , name )
48
135
}
49
136
if len (setting .UI .Themes ) > 0 {
50
137
allowedThemes := container .SetOf (setting .UI .Themes ... )
51
138
for _ , theme := range foundThemes {
52
- if allowedThemes .Contains (theme ) {
139
+ if allowedThemes .Contains (theme . InternalName ) {
53
140
availableThemes = append (availableThemes , theme )
54
141
}
55
142
}
56
143
} else {
57
144
availableThemes = foundThemes
58
145
}
59
- sort .Strings (availableThemes )
146
+ sort .Slice (availableThemes , func (i , j int ) bool {
147
+ if availableThemes [i ].InternalName == setting .UI .DefaultTheme {
148
+ return true
149
+ }
150
+ return availableThemes [i ].DisplayName < availableThemes [j ].DisplayName
151
+ })
60
152
if len (availableThemes ) == 0 {
61
153
setting .LogStartupProblem (1 , log .ERROR , "No theme candidate in asset files, but Gitea requires there should be at least one usable theme" )
62
- availableThemes = []string { setting .UI .DefaultTheme }
154
+ availableThemes = []* ThemeMetaInfo { defaultThemeMetaInfoByInternalName ( setting .UI .DefaultTheme ) }
63
155
}
64
156
}
65
157
66
- func GetAvailableThemes () []string {
158
+ func GetAvailableThemes () []* ThemeMetaInfo {
67
159
themeOnce .Do (initThemes )
68
160
return availableThemes
69
161
}
70
162
71
- func IsThemeAvailable (name string ) bool {
163
+ func IsThemeAvailable (internalName string ) bool {
72
164
themeOnce .Do (initThemes )
73
- return availableThemesSet .Contains (name )
165
+ return availableThemeInternalNames .Contains (internalName )
74
166
}
0 commit comments