aboutsummaryrefslogtreecommitdiffstats
path: root/services
diff options
context:
space:
mode:
authorwxiaoguang <wxiaoguang@gmail.com>2025-03-09 05:38:11 +0800
committerGitHub <noreply@github.com>2025-03-08 21:38:11 +0000
commit6f1333175461a6cf5497965c20b5d81b6e73c5d5 (patch)
tree0fbe0b14a09edbbb188773ff9767ba2648f16a27 /services
parent4c4c56c7cde2316709b478a187a2c97c2f417ccf (diff)
downloadgitea-6f1333175461a6cf5497965c20b5d81b6e73c5d5.tar.gz
gitea-6f1333175461a6cf5497965c20b5d81b6e73c5d5.zip
Improve theme display (#30671)
Document: https://gitea.com/gitea/docs/pulls/180 ![image](https://github.com/go-gitea/gitea/assets/2114189/68e38573-b911-45d9-b7aa-40d96d836ecb)
Diffstat (limited to 'services')
-rw-r--r--services/webtheme/webtheme.go136
-rw-r--r--services/webtheme/webtheme_test.go37
2 files changed, 151 insertions, 22 deletions
diff --git a/services/webtheme/webtheme.go b/services/webtheme/webtheme.go
index dc801e1ff7..58aea3bc74 100644
--- a/services/webtheme/webtheme.go
+++ b/services/webtheme/webtheme.go
@@ -4,6 +4,7 @@
package webtheme
import (
+ "regexp"
"sort"
"strings"
"sync"
@@ -12,63 +13,154 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
)
var (
- availableThemes []string
- availableThemesSet container.Set[string]
- themeOnce sync.Once
+ availableThemes []*ThemeMetaInfo
+ availableThemeInternalNames container.Set[string]
+ themeOnce sync.Once
)
+const (
+ fileNamePrefix = "theme-"
+ fileNameSuffix = ".css"
+)
+
+type ThemeMetaInfo struct {
+ FileName string
+ InternalName string
+ DisplayName string
+}
+
+func parseThemeMetaInfoToMap(cssContent string) map[string]string {
+ /*
+ The theme meta info is stored in the CSS file's variables of `gitea-theme-meta-info` element,
+ which is a privately defined and is only used by backend to extract the meta info.
+ Not using ":root" because it is difficult to parse various ":root" blocks when importing other files,
+ it is difficult to control the overriding, and it's difficult to avoid user's customized overridden styles.
+ */
+ metaInfoContent := cssContent
+ if pos := strings.LastIndex(metaInfoContent, "gitea-theme-meta-info"); pos >= 0 {
+ metaInfoContent = metaInfoContent[pos:]
+ }
+
+ reMetaInfoItem := `
+(
+\s*(--[-\w]+)
+\s*:
+\s*(
+("(\\"|[^"])*")
+|('(\\'|[^'])*')
+|([^'";]+)
+)
+\s*;
+\s*
+)
+`
+ reMetaInfoItem = strings.ReplaceAll(reMetaInfoItem, "\n", "")
+ reMetaInfoBlock := `\bgitea-theme-meta-info\s*\{(` + reMetaInfoItem + `+)\}`
+ re := regexp.MustCompile(reMetaInfoBlock)
+ matchedMetaInfoBlock := re.FindAllStringSubmatch(metaInfoContent, -1)
+ if len(matchedMetaInfoBlock) == 0 {
+ return nil
+ }
+ re = regexp.MustCompile(strings.ReplaceAll(reMetaInfoItem, "\n", ""))
+ matchedItems := re.FindAllStringSubmatch(matchedMetaInfoBlock[0][1], -1)
+ m := map[string]string{}
+ for _, item := range matchedItems {
+ v := item[3]
+ if strings.HasPrefix(v, `"`) {
+ v = strings.TrimSuffix(strings.TrimPrefix(v, `"`), `"`)
+ v = strings.ReplaceAll(v, `\"`, `"`)
+ } else if strings.HasPrefix(v, `'`) {
+ v = strings.TrimSuffix(strings.TrimPrefix(v, `'`), `'`)
+ v = strings.ReplaceAll(v, `\'`, `'`)
+ }
+ m[item[2]] = v
+ }
+ return m
+}
+
+func defaultThemeMetaInfoByFileName(fileName string) *ThemeMetaInfo {
+ themeInfo := &ThemeMetaInfo{
+ FileName: fileName,
+ InternalName: strings.TrimSuffix(strings.TrimPrefix(fileName, fileNamePrefix), fileNameSuffix),
+ }
+ themeInfo.DisplayName = themeInfo.InternalName
+ return themeInfo
+}
+
+func defaultThemeMetaInfoByInternalName(fileName string) *ThemeMetaInfo {
+ return defaultThemeMetaInfoByFileName(fileNamePrefix + fileName + fileNameSuffix)
+}
+
+func parseThemeMetaInfo(fileName, cssContent string) *ThemeMetaInfo {
+ themeInfo := defaultThemeMetaInfoByFileName(fileName)
+ m := parseThemeMetaInfoToMap(cssContent)
+ if m == nil {
+ return themeInfo
+ }
+ themeInfo.DisplayName = m["--theme-display-name"]
+ return themeInfo
+}
+
func initThemes() {
availableThemes = nil
defer func() {
- availableThemesSet = container.SetOf(availableThemes...)
- if !availableThemesSet.Contains(setting.UI.DefaultTheme) {
+ availableThemeInternalNames = container.Set[string]{}
+ for _, theme := range availableThemes {
+ availableThemeInternalNames.Add(theme.InternalName)
+ }
+ if !availableThemeInternalNames.Contains(setting.UI.DefaultTheme) {
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)
}
}()
cssFiles, err := public.AssetFS().ListFiles("/assets/css")
if err != nil {
log.Error("Failed to list themes: %v", err)
- availableThemes = []string{setting.UI.DefaultTheme}
+ availableThemes = []*ThemeMetaInfo{defaultThemeMetaInfoByInternalName(setting.UI.DefaultTheme)}
return
}
- var foundThemes []string
- for _, name := range cssFiles {
- name, ok := strings.CutPrefix(name, "theme-")
- if !ok {
- continue
- }
- name, ok = strings.CutSuffix(name, ".css")
- if !ok {
- continue
+ var foundThemes []*ThemeMetaInfo
+ for _, fileName := range cssFiles {
+ if strings.HasPrefix(fileName, fileNamePrefix) && strings.HasSuffix(fileName, fileNameSuffix) {
+ content, err := public.AssetFS().ReadFile("/assets/css/" + fileName)
+ if err != nil {
+ log.Error("Failed to read theme file %q: %v", fileName, err)
+ continue
+ }
+ foundThemes = append(foundThemes, parseThemeMetaInfo(fileName, util.UnsafeBytesToString(content)))
}
- foundThemes = append(foundThemes, name)
}
if len(setting.UI.Themes) > 0 {
allowedThemes := container.SetOf(setting.UI.Themes...)
for _, theme := range foundThemes {
- if allowedThemes.Contains(theme) {
+ if allowedThemes.Contains(theme.InternalName) {
availableThemes = append(availableThemes, theme)
}
}
} else {
availableThemes = foundThemes
}
- sort.Strings(availableThemes)
+ sort.Slice(availableThemes, func(i, j int) bool {
+ if availableThemes[i].InternalName == setting.UI.DefaultTheme {
+ return true
+ }
+ return availableThemes[i].DisplayName < availableThemes[j].DisplayName
+ })
if len(availableThemes) == 0 {
setting.LogStartupProblem(1, log.ERROR, "No theme candidate in asset files, but Gitea requires there should be at least one usable theme")
- availableThemes = []string{setting.UI.DefaultTheme}
+ availableThemes = []*ThemeMetaInfo{defaultThemeMetaInfoByInternalName(setting.UI.DefaultTheme)}
}
}
-func GetAvailableThemes() []string {
+func GetAvailableThemes() []*ThemeMetaInfo {
themeOnce.Do(initThemes)
return availableThemes
}
-func IsThemeAvailable(name string) bool {
+func IsThemeAvailable(internalName string) bool {
themeOnce.Do(initThemes)
- return availableThemesSet.Contains(name)
+ return availableThemeInternalNames.Contains(internalName)
}
diff --git a/services/webtheme/webtheme_test.go b/services/webtheme/webtheme_test.go
new file mode 100644
index 0000000000..587953ab0c
--- /dev/null
+++ b/services/webtheme/webtheme_test.go
@@ -0,0 +1,37 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package webtheme
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParseThemeMetaInfo(t *testing.T) {
+ m := parseThemeMetaInfoToMap(`gitea-theme-meta-info {
+ --k1: "v1";
+ --k2: "v\"2";
+ --k3: 'v3';
+ --k4: 'v\'4';
+ --k5: v5;
+}`)
+ assert.Equal(t, map[string]string{
+ "--k1": "v1",
+ "--k2": `v"2`,
+ "--k3": "v3",
+ "--k4": "v'4",
+ "--k5": "v5",
+ }, m)
+
+ // if an auto theme imports others, the meta info should be extracted from the last one
+ // the meta in imported themes should be ignored to avoid incorrect overriding
+ m = parseThemeMetaInfoToMap(`
+@media (prefers-color-scheme: dark) { gitea-theme-meta-info { --k1: foo; } }
+@media (prefers-color-scheme: light) { gitea-theme-meta-info { --k1: bar; } }
+gitea-theme-meta-info {
+ --k2: real;
+}`)
+ assert.Equal(t, map[string]string{"--k2": "real"}, m)
+}