4
4
package webtheme
5
5
6
6
import (
7
+ "regexp"
7
8
"sort"
8
9
"strings"
9
10
"sync"
@@ -12,63 +13,156 @@ 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
+ PreferColorSchemes container.Set [string ]
35
+ }
36
+
37
+ func parseThemeMetaInfoToMap (cssContent string ) map [string ]string {
38
+ metaInfoContent := cssContent
39
+ if pos := strings .LastIndex (metaInfoContent , "gitea-theme-meta-info" ); pos >= 0 {
40
+ metaInfoContent = metaInfoContent [pos :]
41
+ }
42
+
43
+ reMetaInfoItem := `
44
+ (
45
+ \s*(--[-\w]+)
46
+ \s*:
47
+ \s*("(\\"|[^"])*")
48
+ \s*;
49
+ \s*
50
+ )
51
+ `
52
+ reMetaInfoItem = strings .ReplaceAll (reMetaInfoItem , "\n " , "" )
53
+ reMetaInfoBlock := `\bgitea-theme-meta-info\s*\{(` + reMetaInfoItem + `+)\}`
54
+ re := regexp .MustCompile (reMetaInfoBlock )
55
+ matchedMetaInfoBlock := re .FindAllStringSubmatch (metaInfoContent , - 1 )
56
+ if len (matchedMetaInfoBlock ) == 0 {
57
+ return nil
58
+ }
59
+ re = regexp .MustCompile (strings .ReplaceAll (reMetaInfoItem , "\n " , "" ))
60
+ matchedItems := re .FindAllStringSubmatch (matchedMetaInfoBlock [0 ][1 ], - 1 )
61
+ m := map [string ]string {}
62
+ for _ , item := range matchedItems {
63
+ v := item [3 ]
64
+ v = strings .TrimPrefix (v , "\" " )
65
+ v = strings .TrimSuffix (v , "\" " )
66
+ v = strings .ReplaceAll (v , `\"` , `"` )
67
+ m [item [2 ]] = v
68
+ }
69
+ return m
70
+ }
71
+
72
+ // @media (prefers-color-scheme: dark)
73
+ func parseThemePreferColorSchemes (cssContent string ) container.Set [string ] {
74
+ re := regexp .MustCompile (`@media\s*\(\s*prefers-color-scheme\s*:\s*([-\w]+)\s*\)` )
75
+ matched := re .FindAllStringSubmatch (cssContent , - 1 )
76
+ if len (matched ) == 0 {
77
+ return nil
78
+ }
79
+ schemes := container.Set [string ]{}
80
+ for _ , m := range matched {
81
+ schemes .Add (m [1 ])
82
+ }
83
+ return schemes
84
+ }
85
+
86
+ func defaultThemeMetaInfoByFileName (fileName string ) * ThemeMetaInfo {
87
+ themeInfo := & ThemeMetaInfo {
88
+ FileName : fileName ,
89
+ InternalName : strings .TrimSuffix (strings .TrimPrefix (fileName , fileNamePrefix ), fileNameSuffix ),
90
+ }
91
+ themeInfo .DisplayName = themeInfo .InternalName
92
+ return themeInfo
93
+ }
94
+
95
+ func defaultThemeMetaInfoByInternalName (fileName string ) * ThemeMetaInfo {
96
+ return defaultThemeMetaInfoByFileName (fileNamePrefix + fileName + fileNameSuffix )
97
+ }
98
+
99
+ func parseThemeMetaInfo (fileName , cssContent string ) * ThemeMetaInfo {
100
+ themeInfo := defaultThemeMetaInfoByFileName (fileName )
101
+ themeInfo .PreferColorSchemes = parseThemePreferColorSchemes (cssContent )
102
+ m := parseThemeMetaInfoToMap (cssContent )
103
+ if m == nil {
104
+ return themeInfo
105
+ }
106
+ themeInfo .DisplayName = m ["--theme-display-name" ]
107
+ return themeInfo
108
+ }
109
+
23
110
func initThemes () {
24
111
availableThemes = nil
25
112
defer func () {
26
- availableThemesSet = container .SetOf (availableThemes ... )
27
- if ! availableThemesSet .Contains (setting .UI .DefaultTheme ) {
113
+ availableThemeInternalNames = container.Set [string ]{}
114
+ for _ , theme := range availableThemes {
115
+ availableThemeInternalNames .Add (theme .InternalName )
116
+ }
117
+ if ! availableThemeInternalNames .Contains (setting .UI .DefaultTheme ) {
28
118
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
119
}
30
120
}()
31
121
cssFiles , err := public .AssetFS ().ListFiles ("/assets/css" )
32
122
if err != nil {
33
123
log .Error ("Failed to list themes: %v" , err )
34
- availableThemes = []string { setting .UI .DefaultTheme }
124
+ availableThemes = []* ThemeMetaInfo { defaultThemeMetaInfoByInternalName ( setting .UI .DefaultTheme ) }
35
125
return
36
126
}
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
127
+ var foundThemes []* ThemeMetaInfo
128
+ for _ , fileName := range cssFiles {
129
+ if strings . HasPrefix ( fileName , fileNamePrefix ) && strings .HasSuffix ( fileName , fileNameSuffix ) {
130
+ content , err := public . AssetFS (). ReadFile ( "/assets/css/" + fileName )
131
+ if err != nil {
132
+ log . Error ( "Failed to read theme file %q: %v" , fileName , err )
133
+ continue
134
+ }
135
+ foundThemes = append ( foundThemes , parseThemeMetaInfo ( fileName , util . UnsafeBytesToString ( content )))
46
136
}
47
- foundThemes = append (foundThemes , name )
48
137
}
49
138
if len (setting .UI .Themes ) > 0 {
50
139
allowedThemes := container .SetOf (setting .UI .Themes ... )
51
140
for _ , theme := range foundThemes {
52
- if allowedThemes .Contains (theme ) {
141
+ if allowedThemes .Contains (theme . InternalName ) {
53
142
availableThemes = append (availableThemes , theme )
54
143
}
55
144
}
56
145
} else {
57
146
availableThemes = foundThemes
58
147
}
59
- sort .Strings (availableThemes )
148
+ sort .Slice (availableThemes , func (i , j int ) bool {
149
+ if availableThemes [i ].InternalName == setting .UI .DefaultTheme {
150
+ return true
151
+ }
152
+ return availableThemes [i ].DisplayName < availableThemes [j ].DisplayName
153
+ })
60
154
if len (availableThemes ) == 0 {
61
155
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 }
156
+ availableThemes = []* ThemeMetaInfo { defaultThemeMetaInfoByInternalName ( setting .UI .DefaultTheme ) }
63
157
}
64
158
}
65
159
66
- func GetAvailableThemes () []string {
160
+ func GetAvailableThemes () []* ThemeMetaInfo {
67
161
themeOnce .Do (initThemes )
68
162
return availableThemes
69
163
}
70
164
71
- func IsThemeAvailable (name string ) bool {
165
+ func IsThemeAvailable (internalName string ) bool {
72
166
themeOnce .Do (initThemes )
73
- return availableThemesSet .Contains (name )
167
+ return availableThemeInternalNames .Contains (internalName )
74
168
}
0 commit comments