44package webtheme
55
66import (
7- "context "
7+ "regexp "
88 "sort"
99 "strings"
1010 "sync"
@@ -13,67 +13,154 @@ import (
1313 "code.gitea.io/gitea/modules/log"
1414 "code.gitea.io/gitea/modules/public"
1515 "code.gitea.io/gitea/modules/setting"
16+ "code.gitea.io/gitea/modules/util"
1617)
1718
1819var (
19- availableThemes [] string
20- availableThemesSet container.Set [string ]
21- themeOnce sync.Once
20+ availableThemes [] * ThemeMetaInfo
21+ availableThemeInternalNames container.Set [string ]
22+ themeOnce sync.Once
2223)
2324
24- func initThemes (ctx context.Context ) {
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+
108+ func initThemes () {
25109 availableThemes = nil
26110 defer func () {
27- availableThemesSet = container .SetOf (availableThemes ... )
28- if ! availableThemesSet .Contains (setting .Config ().UI .DefaultTheme .Value (ctx )) {
29- setting .LogStartupProblem (1 , log .ERROR , "Default theme %q is not available, please correct the '[ui].DEFAULT_THEME' setting in the config file" , setting .Config ().UI .DefaultTheme .Value (ctx ))
111+ availableThemeInternalNames = container.Set [string ]{}
112+ for _ , theme := range availableThemes {
113+ availableThemeInternalNames .Add (theme .InternalName )
114+ }
115+ if ! availableThemeInternalNames .Contains (setting .UI .DefaultTheme ) {
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 )
30117 }
31118 }()
32119 cssFiles , err := public .AssetFS ().ListFiles ("/assets/css" )
33120 if err != nil {
34121 log .Error ("Failed to list themes: %v" , err )
35- availableThemes = []string { setting .Config (). UI .DefaultTheme . Value ( ctx )}
122+ availableThemes = []* ThemeMetaInfo { defaultThemeMetaInfoByInternalName ( setting .UI .DefaultTheme )}
36123 return
37124 }
38- var foundThemes []string
39- for _ , name := range cssFiles {
40- name , ok := strings .CutPrefix ( name , "theme-" )
41- if ! ok {
42- continue
43- }
44- name , ok = strings . CutSuffix ( name , ".css" )
45- if ! ok {
46- 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 )))
47134 }
48- foundThemes = append (foundThemes , name )
49135 }
50- if len (setting .Config (). UI .Themes . Value ( ctx ) ) > 0 {
51- allowedThemes := container .SetOf (setting .Config (). UI .Themes . Value ( ctx ) ... )
136+ if len (setting .UI .Themes ) > 0 {
137+ allowedThemes := container .SetOf (setting .UI .Themes ... )
52138 for _ , theme := range foundThemes {
53- if allowedThemes .Contains (theme ) {
139+ if allowedThemes .Contains (theme . InternalName ) {
54140 availableThemes = append (availableThemes , theme )
55141 }
56142 }
57143 } else {
58144 availableThemes = foundThemes
59145 }
60- 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+ })
61152 if len (availableThemes ) == 0 {
62153 setting .LogStartupProblem (1 , log .ERROR , "No theme candidate in asset files, but Gitea requires there should be at least one usable theme" )
63- availableThemes = []string { setting .Config (). UI .DefaultTheme . Value ( ctx )}
154+ availableThemes = []* ThemeMetaInfo { defaultThemeMetaInfoByInternalName ( setting .UI .DefaultTheme )}
64155 }
65156}
66157
67- func GetAvailableThemes (ctx context.Context ) []string {
68- themeOnce .Do (func () {
69- initThemes (ctx )
70- })
158+ func GetAvailableThemes () []* ThemeMetaInfo {
159+ themeOnce .Do (initThemes )
71160 return availableThemes
72161}
73162
74- func IsThemeAvailable (ctx context.Context , name string ) bool {
75- themeOnce .Do (func () {
76- initThemes (ctx )
77- })
78- return availableThemesSet .Contains (name )
163+ func IsThemeAvailable (internalName string ) bool {
164+ themeOnce .Do (initThemes )
165+ return availableThemeInternalNames .Contains (internalName )
79166}
0 commit comments