diff options
author | wxiaoguang <wxiaoguang@gmail.com> | 2025-03-09 05:38:11 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-03-08 21:38:11 +0000 |
commit | 6f1333175461a6cf5497965c20b5d81b6e73c5d5 (patch) | |
tree | 0fbe0b14a09edbbb188773ff9767ba2648f16a27 /services | |
parent | 4c4c56c7cde2316709b478a187a2c97c2f417ccf (diff) | |
download | gitea-6f1333175461a6cf5497965c20b5d81b6e73c5d5.tar.gz gitea-6f1333175461a6cf5497965c20b5d81b6e73c5d5.zip |
Improve theme display (#30671)
Document: https://gitea.com/gitea/docs/pulls/180

Diffstat (limited to 'services')
-rw-r--r-- | services/webtheme/webtheme.go | 136 | ||||
-rw-r--r-- | services/webtheme/webtheme_test.go | 37 |
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) +} |