44package webtheme
55
66import (
7+ "regexp"
78 "sort"
89 "strings"
910 "sync"
@@ -12,63 +13,156 @@ 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+ 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+
23110func initThemes () {
24111 availableThemes = nil
25112 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 ) {
28118 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 )
29119 }
30120 }()
31121 cssFiles , err := public .AssetFS ().ListFiles ("/assets/css" )
32122 if err != nil {
33123 log .Error ("Failed to list themes: %v" , err )
34- availableThemes = []string { setting .UI .DefaultTheme }
124+ availableThemes = []* ThemeMetaInfo { defaultThemeMetaInfoByInternalName ( setting .UI .DefaultTheme ) }
35125 return
36126 }
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 )))
46136 }
47- foundThemes = append (foundThemes , name )
48137 }
49138 if len (setting .UI .Themes ) > 0 {
50139 allowedThemes := container .SetOf (setting .UI .Themes ... )
51140 for _ , theme := range foundThemes {
52- if allowedThemes .Contains (theme ) {
141+ if allowedThemes .Contains (theme . InternalName ) {
53142 availableThemes = append (availableThemes , theme )
54143 }
55144 }
56145 } else {
57146 availableThemes = foundThemes
58147 }
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+ })
60154 if len (availableThemes ) == 0 {
61155 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 ) }
63157 }
64158}
65159
66- func GetAvailableThemes () []string {
160+ func GetAvailableThemes () []* ThemeMetaInfo {
67161 themeOnce .Do (initThemes )
68162 return availableThemes
69163}
70164
71- func IsThemeAvailable (name string ) bool {
165+ func IsThemeAvailable (internalName string ) bool {
72166 themeOnce .Do (initThemes )
73- return availableThemesSet .Contains (name )
167+ return availableThemeInternalNames .Contains (internalName )
74168}
0 commit comments