A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
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}