44package webtheme
55
66import (
7+ "regexp"
78 "sort"
89 "strings"
910 "sync"
@@ -12,63 +13,154 @@ import (
1213 "code.gitea.io/gitea/modules/log"
1314 "code.gitea.io/gitea/modules/public"
1415 "code.gitea.io/gitea/modules/setting"
16+ "code.gitea.io/gitea/modules/util"
1517)
1618
1719var (
18- availableThemes [] string
19- availableThemesSet container.Set [string ]
20- themeOnce sync.Once
20+ availableThemes [] * ThemeMetaInfo
21+ availableThemeInternalNames container.Set [string ]
22+ themeOnce sync.Once
2123)
2224
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+
23108func initThemes () {
24109 availableThemes = nil
25110 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 ) {
28116 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 )
29117 }
30118 }()
31119 cssFiles , err := public .AssetFS ().ListFiles ("/assets/css" )
32120 if err != nil {
33121 log .Error ("Failed to list themes: %v" , err )
34- availableThemes = []string { setting .UI .DefaultTheme }
122+ availableThemes = []* ThemeMetaInfo { defaultThemeMetaInfoByInternalName ( setting .UI .DefaultTheme ) }
35123 return
36124 }
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 )))
46134 }
47- foundThemes = append (foundThemes , name )
48135 }
49136 if len (setting .UI .Themes ) > 0 {
50137 allowedThemes := container .SetOf (setting .UI .Themes ... )
51138 for _ , theme := range foundThemes {
52- if allowedThemes .Contains (theme ) {
139+ if allowedThemes .Contains (theme . InternalName ) {
53140 availableThemes = append (availableThemes , theme )
54141 }
55142 }
56143 } else {
57144 availableThemes = foundThemes
58145 }
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+ })
60152 if len (availableThemes ) == 0 {
61153 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 ) }
63155 }
64156}
65157
66- func GetAvailableThemes () []string {
158+ func GetAvailableThemes () []* ThemeMetaInfo {
67159 themeOnce .Do (initThemes )
68160 return availableThemes
69161}
70162
71- func IsThemeAvailable (name string ) bool {
163+ func IsThemeAvailable (internalName string ) bool {
72164 themeOnce .Do (initThemes )
73- return availableThemesSet .Contains (name )
165+ return availableThemeInternalNames .Contains (internalName )
74166}
0 commit comments