A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 234 lines 6.6 kB view raw
1// SiYuan - Refactor your thinking 2// Copyright (c) 2020-present, b3log.org 3// 4// This program is free software: you can redistribute it and/or modify 5// it under the terms of the GNU Affero General Public License as published by 6// the Free Software Foundation, either version 3 of the License, or 7// (at your option) any later version. 8// 9// This program is distributed in the hope that it will be useful, 10// but WITHOUT ANY WARRANTY; without even the implied warranty of 11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12// GNU Affero General Public License for more details. 13// 14// You should have received a copy of the GNU Affero General Public License 15// along with this program. If not, see <https://www.gnu.org/licenses/>. 16 17package model 18 19import ( 20 "bytes" 21 "os" 22 "path/filepath" 23 "strings" 24 25 "github.com/88250/gulu" 26 "github.com/88250/lute/ast" 27 "github.com/88250/lute/parse" 28 "github.com/gorilla/css/scanner" 29 "github.com/siyuan-note/logging" 30 "github.com/siyuan-note/siyuan/kernel/util" 31 "github.com/vanng822/css" 32) 33 34// 将文档中的 CSS 变量替换为具体的主题样式值 35func fillThemeStyleVar(tree *parse.Tree) { 36 if nil == tree || nil == tree.Root { 37 return 38 } 39 40 var themeStyles map[string]string 41 if 1 == Conf.Appearance.Mode { 42 themeStyles = getThemeStyleVar(Conf.Appearance.ThemeDark, true) 43 } else { 44 themeStyles = getThemeStyleVar(Conf.Appearance.ThemeLight, false) 45 } 46 if 1 > len(themeStyles) { 47 return 48 } 49 50 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { 51 if !entering { 52 return ast.WalkContinue 53 } 54 55 // 遍历节点的 Kramdown IAL (Inline Attribute List) 属性 56 for _, ial := range n.KramdownIAL { 57 if "style" != ial[0] { 58 continue 59 } 60 61 styleSheet := css.Parse(ial[1]) 62 buf := bytes.Buffer{} 63 for _, r := range styleSheet.GetCSSRuleList() { 64 styles := getStyleVarName(r.Style.Selector) 65 for style, name := range styles { 66 buf.WriteString(style) 67 buf.WriteString(": ") 68 69 // 解析嵌套的 CSS 变量 70 value := resolveNestedCSSVar(themeStyles, name) 71 72 if "" == value { 73 // 回退为原始 var() 形式 74 buf.WriteString("var(") 75 buf.WriteString(name) 76 buf.WriteString(")") 77 } else { 78 buf.WriteString(value) 79 } 80 buf.WriteString("; ") 81 } 82 } 83 if 0 < buf.Len() { 84 ial[1] = strings.TrimSpace(buf.String()) 85 } 86 } 87 return ast.WalkContinue 88 }) 89} 90 91// 递归解析嵌套的 CSS 变量 92func resolveNestedCSSVar(themeStyles map[string]string, varName string) string { 93 visited := make(map[string]bool) // 循环引用检测 94 maxDepth := 10 // 防止无限嵌套 95 96 currentName := varName 97 for depth := 0; depth < maxDepth; depth++ { 98 if visited[currentName] { 99 return "" 100 } 101 visited[currentName] = true 102 103 value, exists := themeStyles[currentName] 104 if !exists { 105 return "" 106 } 107 108 // 如果不包含嵌套变量,直接返回最终值 109 if !strings.Contains(value, "var(") { 110 return value 111 } 112 113 // 提取嵌套变量名:var(--variable-name) -> --variable-name 114 nestedVarName := gulu.Str.SubStringBetween(value, "(", ")") 115 if "" == nestedVarName { 116 return value 117 } 118 119 currentName = nestedVarName 120 } 121 122 return "" 123} 124 125// 从 CSS 选择器值中解析出样式属性和对应的 CSS 变量名 126func getStyleVarName(value *css.CSSValue) (ret map[string]string) { 127 ret = map[string]string{} 128 129 var start, end int 130 var style, name string 131 for i, t := range value.Tokens { 132 // 获取样式属性名 133 if scanner.TokenIdent == t.Type && 0 == start { 134 style = strings.TrimSpace(t.Value) 135 continue 136 } 137 138 if scanner.TokenFunction == t.Type && "var(" == t.Value { 139 start = i 140 continue 141 } 142 if scanner.TokenChar == t.Type && ")" == t.Value { 143 end = i 144 145 // 提取 var() 中的变量名 146 if 0 < start && 0 < end { 147 for _, tt := range value.Tokens[start+1 : end] { 148 name += tt.Value 149 } 150 name = strings.TrimSpace(name) 151 } 152 start, end = 0, 0 153 ret[style] = name 154 style, name = "", "" 155 } 156 } 157 return 158} 159 160// 获取主题的样式变量映射表 161func getThemeStyleVar(theme string, isDarkMode bool) (ret map[string]string) { 162 ret = map[string]string{} 163 164 var cssContent string 165 166 // 第三方主题可能缺少基础变量,先加载默认主题作为基础 167 defaultTheme := map[bool]string{false: "daylight", true: "midnight"}[isDarkMode] 168 if theme != defaultTheme { 169 defaultData, err := os.ReadFile(filepath.Join(util.ThemesPath, defaultTheme, "theme.css")) 170 if err != nil { 171 logging.LogErrorf("read default theme [%s] css file failed: %s", defaultTheme, err) 172 } else { 173 cssContent = string(defaultData) + "\n" 174 } 175 } 176 177 // 拼接主题 CSS,后面的规则覆盖前面的规则 178 userData, err := os.ReadFile(filepath.Join(util.ThemesPath, theme, "theme.css")) 179 if err != nil { 180 logging.LogErrorf("read theme [%s] css file failed: %s", theme, err) 181 return ret 182 } 183 cssContent += string(userData) 184 185 // 解析拼接后的完整 CSS 内容 186 styleSheet := css.Parse(cssContent) 187 stylePriorities := map[string]int{} 188 currentMode := map[bool]string{false: "light", true: "dark"}[isDarkMode] 189 for _, rule := range styleSheet.GetCSSRuleList() { 190 priority := getSelectorPriority(rule.Style.Selector.Text(), currentMode) 191 for _, style := range rule.Style.Styles { 192 propName := style.Property 193 propValue := strings.TrimSpace(style.Value.Text()) 194 195 if existingPriority, exists := stylePriorities[propName]; !exists || priority >= existingPriority { 196 ret[propName] = propValue 197 stylePriorities[propName] = priority 198 } 199 200 // 如果两个短横线开头 CSS 解析器有问题,--b3-theme-primary: #3575f0; 会被解析为 -b3-theme-primary:- #3575f0 201 // 这里两种解析都放到结果中 202 bugFixPropName := "-" + propName 203 bugFixPropValue := strings.TrimSpace(strings.TrimPrefix(propValue, "-")) 204 if existingPriority, exists := stylePriorities[bugFixPropName]; !exists || priority >= existingPriority { 205 ret[bugFixPropName] = bugFixPropValue 206 stylePriorities[bugFixPropName] = priority 207 } 208 } 209 } 210 return ret 211} 212 213// 粗略计算 CSS 选择器的优先级 214func getSelectorPriority(selector, currentMode string) int { 215 selector = strings.TrimSpace(strings.ToLower(selector)) 216 217 modeSelectors := []string{ 218 "[data-theme-mode=\"" + currentMode + "\"]", 219 "[data-theme-mode='" + currentMode + "']", 220 "[data-theme-mode=" + currentMode + "]", 221 } 222 223 for _, modeSelector := range modeSelectors { 224 if strings.Contains(selector, modeSelector) { 225 if strings.Contains(selector, ":root") || strings.Contains(selector, "html") { 226 return 2 227 } else { 228 return 1 229 } 230 } 231 } 232 233 return 0 234}