gitea源码

webtheme.go 5.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. // Copyright 2024 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package webtheme
  4. import (
  5. "regexp"
  6. "sort"
  7. "strings"
  8. "sync"
  9. "code.gitea.io/gitea/modules/container"
  10. "code.gitea.io/gitea/modules/log"
  11. "code.gitea.io/gitea/modules/public"
  12. "code.gitea.io/gitea/modules/setting"
  13. "code.gitea.io/gitea/modules/util"
  14. )
  15. var (
  16. availableThemes []*ThemeMetaInfo
  17. availableThemeInternalNames container.Set[string]
  18. themeOnce sync.Once
  19. )
  20. const (
  21. fileNamePrefix = "theme-"
  22. fileNameSuffix = ".css"
  23. )
  24. type ThemeMetaInfo struct {
  25. FileName string
  26. InternalName string
  27. DisplayName string
  28. }
  29. func parseThemeMetaInfoToMap(cssContent string) map[string]string {
  30. /*
  31. The theme meta info is stored in the CSS file's variables of `gitea-theme-meta-info` element,
  32. which is a privately defined and is only used by backend to extract the meta info.
  33. Not using ":root" because it is difficult to parse various ":root" blocks when importing other files,
  34. it is difficult to control the overriding, and it's difficult to avoid user's customized overridden styles.
  35. */
  36. metaInfoContent := cssContent
  37. if pos := strings.LastIndex(metaInfoContent, "gitea-theme-meta-info"); pos >= 0 {
  38. metaInfoContent = metaInfoContent[pos:]
  39. }
  40. reMetaInfoItem := `
  41. (
  42. \s*(--[-\w]+)
  43. \s*:
  44. \s*(
  45. ("(\\"|[^"])*")
  46. |('(\\'|[^'])*')
  47. |([^'";]+)
  48. )
  49. \s*;
  50. \s*
  51. )
  52. `
  53. reMetaInfoItem = strings.ReplaceAll(reMetaInfoItem, "\n", "")
  54. reMetaInfoBlock := `\bgitea-theme-meta-info\s*\{(` + reMetaInfoItem + `+)\}`
  55. re := regexp.MustCompile(reMetaInfoBlock)
  56. matchedMetaInfoBlock := re.FindAllStringSubmatch(metaInfoContent, -1)
  57. if len(matchedMetaInfoBlock) == 0 {
  58. return nil
  59. }
  60. re = regexp.MustCompile(strings.ReplaceAll(reMetaInfoItem, "\n", ""))
  61. matchedItems := re.FindAllStringSubmatch(matchedMetaInfoBlock[0][1], -1)
  62. m := map[string]string{}
  63. for _, item := range matchedItems {
  64. v := item[3]
  65. if after, ok := strings.CutPrefix(v, `"`); ok {
  66. v = strings.TrimSuffix(after, `"`)
  67. v = strings.ReplaceAll(v, `\"`, `"`)
  68. } else if after, ok := strings.CutPrefix(v, `'`); ok {
  69. v = strings.TrimSuffix(after, `'`)
  70. v = strings.ReplaceAll(v, `\'`, `'`)
  71. }
  72. m[item[2]] = v
  73. }
  74. return m
  75. }
  76. func defaultThemeMetaInfoByFileName(fileName string) *ThemeMetaInfo {
  77. themeInfo := &ThemeMetaInfo{
  78. FileName: fileName,
  79. InternalName: strings.TrimSuffix(strings.TrimPrefix(fileName, fileNamePrefix), fileNameSuffix),
  80. }
  81. themeInfo.DisplayName = themeInfo.InternalName
  82. return themeInfo
  83. }
  84. func defaultThemeMetaInfoByInternalName(fileName string) *ThemeMetaInfo {
  85. return defaultThemeMetaInfoByFileName(fileNamePrefix + fileName + fileNameSuffix)
  86. }
  87. func parseThemeMetaInfo(fileName, cssContent string) *ThemeMetaInfo {
  88. themeInfo := defaultThemeMetaInfoByFileName(fileName)
  89. m := parseThemeMetaInfoToMap(cssContent)
  90. if m == nil {
  91. return themeInfo
  92. }
  93. themeInfo.DisplayName = m["--theme-display-name"]
  94. return themeInfo
  95. }
  96. func initThemes() {
  97. availableThemes = nil
  98. defer func() {
  99. availableThemeInternalNames = container.Set[string]{}
  100. for _, theme := range availableThemes {
  101. availableThemeInternalNames.Add(theme.InternalName)
  102. }
  103. if !availableThemeInternalNames.Contains(setting.UI.DefaultTheme) {
  104. 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)
  105. }
  106. }()
  107. cssFiles, err := public.AssetFS().ListFiles("/assets/css")
  108. if err != nil {
  109. log.Error("Failed to list themes: %v", err)
  110. availableThemes = []*ThemeMetaInfo{defaultThemeMetaInfoByInternalName(setting.UI.DefaultTheme)}
  111. return
  112. }
  113. var foundThemes []*ThemeMetaInfo
  114. for _, fileName := range cssFiles {
  115. if strings.HasPrefix(fileName, fileNamePrefix) && strings.HasSuffix(fileName, fileNameSuffix) {
  116. content, err := public.AssetFS().ReadFile("/assets/css/" + fileName)
  117. if err != nil {
  118. log.Error("Failed to read theme file %q: %v", fileName, err)
  119. continue
  120. }
  121. foundThemes = append(foundThemes, parseThemeMetaInfo(fileName, util.UnsafeBytesToString(content)))
  122. }
  123. }
  124. if len(setting.UI.Themes) > 0 {
  125. allowedThemes := container.SetOf(setting.UI.Themes...)
  126. for _, theme := range foundThemes {
  127. if allowedThemes.Contains(theme.InternalName) {
  128. availableThemes = append(availableThemes, theme)
  129. }
  130. }
  131. } else {
  132. availableThemes = foundThemes
  133. }
  134. sort.Slice(availableThemes, func(i, j int) bool {
  135. if availableThemes[i].InternalName == setting.UI.DefaultTheme {
  136. return true
  137. }
  138. return availableThemes[i].DisplayName < availableThemes[j].DisplayName
  139. })
  140. if len(availableThemes) == 0 {
  141. setting.LogStartupProblem(1, log.ERROR, "No theme candidate in asset files, but Gitea requires there should be at least one usable theme")
  142. availableThemes = []*ThemeMetaInfo{defaultThemeMetaInfoByInternalName(setting.UI.DefaultTheme)}
  143. }
  144. }
  145. func GetAvailableThemes() []*ThemeMetaInfo {
  146. themeOnce.Do(initThemes)
  147. return availableThemes
  148. }
  149. func IsThemeAvailable(internalName string) bool {
  150. themeOnce.Do(initThemes)
  151. return availableThemeInternalNames.Contains(internalName)
  152. }