// SiYuan - Build Your Eternal Digital Garden // Copyright (c) 2020-present, b3log.org // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package model import ( "bytes" "fmt" "os" "path/filepath" "strings" "time" "github.com/88250/css" "github.com/88250/gulu" "github.com/siyuan-note/siyuan/kernel/util" ) var colorKeys = map[string][]string{ "colorPrimary": colorPrimary, "colorFont": colorFont, "colorBorder": colorBorder, "colorScroll": colorScroll, "colorTab": colorTab, "colorTip": colorTip, "colorGraph": colorGraph, "colorInline": colorInline, } var colorPrimary = []string{ "--b3-theme-primary", "--b3-theme-primary-light", "--b3-theme-primary-lighter", "--b3-theme-primary-lightest", "--b3-theme-secondary", "--b3-theme-background", "--b3-theme-surface", "--b3-theme-error", } var colorFont = []string{ "--b3-theme-on-primary", "--b3-theme-on-secondary", "--b3-theme-on-background", "--b3-theme-on-surface", "--b3-theme-on-error", } var colorBorder = []string{ "--b3-border-color", } var colorScroll = []string{ "--b3-scroll-color", } var colorTab = []string{ "--b3-tab-background", } var colorTip = []string{ "--b3-tooltips-color", } var colorGraph = []string{ "--b3-graph-line", "--b3-graph-hl-point", "--b3-graph-hl-line", "--b3-graph-p-point", "--b3-graph-heading-point", "--b3-graph-math-point", "--b3-graph-code-point", "--b3-graph-table-point", "--b3-graph-list-point", "--b3-graph-todo-point", "--b3-graph-olist-point", "--b3-graph-listitem-point", "--b3-graph-bq-point", "--b3-graph-super-point", "--b3-graph-doc-point", "--b3-graph-tag-point", "--b3-graph-asset-point", "--b3-graph-line", "--b3-graph-tag-line", "--b3-graph-ref-line", "--b3-graph-tag-tag-line", "--b3-graph-asset-line", "--b3-graph-hl-point", "--b3-graph-hl-line", } var colorInline = []string{ "--b3-protyle-inline-strong-color", "--b3-protyle-inline-em-color", "--b3-protyle-inline-s-color", "--b3-protyle-inline-link-color", "--b3-protyle-inline-tag-color", "--b3-protyle-inline-blockref-color", "--b3-protyle-inline-mark-background", "--b3-protyle-inline-mark-color", } func currentCSSValue(key string) string { var themeName string if 0 == Conf.Appearance.Mode { themeName = Conf.Appearance.ThemeLight } else { themeName = Conf.Appearance.ThemeDark } themePath := filepath.Join(util.ThemesPath, themeName) theme := filepath.Join(themePath, "theme.css") custom := filepath.Join(themePath, "custom.css") var data []byte var err error if Conf.Appearance.CustomCSS { data, _ = os.ReadFile(custom) } if 1 > len(data) { data, err = os.ReadFile(theme) if nil != err { util.LogErrorf("read theme css [%s] failed: %s", theme, err) return "#ffffff" } } ss := css.Parse(string(data)) rules := ss.GetCSSRuleList() for _, rule := range rules { for _, style := range rule.Style.Styles { fixStyle(style) if key == style.Property { return style.Value.Text() } } } return "" } func ReadCustomCSS(themeName string) (ret map[string]map[string]string, err error) { ret = map[string]map[string]string{} themePath := filepath.Join(util.ThemesPath, themeName) theme := filepath.Join(themePath, "theme.css") custom := filepath.Join(themePath, "custom.css") if !gulu.File.IsExist(custom) { if err = gulu.File.CopyFile(theme, custom); nil != err { util.LogErrorf("copy theme [%s] to [%s] failed: %s", theme, custom, err) return } } data, err := os.ReadFile(custom) if nil != err { util.LogErrorf("read custom css [%s] failed: %s", custom, err) return } fullColorMap := map[string]string{} ss := css.Parse(string(data)) rules := ss.GetCSSRuleList() for _, rule := range rules { for _, style := range rule.Style.Styles { fixStyle(style) fullColorMap[style.Property] = style.Value.Text() } } // 补充现有主题中的样式 data, err = os.ReadFile(theme) if nil != err { util.LogErrorf("read theme css [%s] failed: %s", theme, err) return } ss = css.Parse(string(data)) rules = ss.GetCSSRuleList() for _, rule := range rules { for _, style := range rule.Style.Styles { fixStyle(style) if _, ok := fullColorMap[style.Property]; !ok { fullColorMap[style.Property] = style.Value.ParsedText() } } } buildColor(&ret, fullColorMap, "colorPrimary") buildColor(&ret, fullColorMap, "colorFont") buildColor(&ret, fullColorMap, "colorBorder") buildColor(&ret, fullColorMap, "colorScroll") buildColor(&ret, fullColorMap, "colorTab") buildColor(&ret, fullColorMap, "colorTip") buildColor(&ret, fullColorMap, "colorGraph") buildColor(&ret, fullColorMap, "colorInline") return } func buildColor(ret *map[string]map[string]string, fullColorMap map[string]string, colorMapKey string) { colorMap := map[string]string{} for _, colorKey := range colorKeys[colorMapKey] { colorMap[colorKey] = fullColorMap[colorKey] } (*ret)[colorMapKey] = colorMap } func WriteCustomCSS(themeName string, cssMap map[string]interface{}) (err error) { customCSS := map[string]string{} for _, vMap := range cssMap { cssKV := vMap.(map[string]interface{}) for k, v := range cssKV { customCSS[k] = v.(string) } } themePath := filepath.Join(util.ThemesPath, themeName) custom := filepath.Join(themePath, "custom.css") data, err := os.ReadFile(custom) if nil != err { util.LogErrorf("read custom css [%s] failed: %s", custom, err) return } cssData := util.RemoveInvisible(string(data)) customStyleSheet := css.Parse(cssData) buf := &bytes.Buffer{} customRules := customStyleSheet.CssRuleList for _, customRule := range customRules { if css.KEYFRAMES_RULE == customRule.Type { keyframes(customRule, buf) continue } else if css.STYLE_RULE != customRule.Type { buf.WriteString(customRule.Type.Text()) buf.WriteString(customRule.Style.Text()) buf.WriteString("\n\n") continue } for _, style := range customRule.Style.Styles { fixStyle(style) if val, ok := customCSS[style.Property]; ok { style.Value = css.NewCSSValue(val) delete(customCSS, style.Property) } } for k, v := range customCSS { customRule.Style.Styles = append(customRule.Style.Styles, &css.CSSStyleDeclaration{Property: k, Value: css.NewCSSValue(v)}) } buf.WriteString(customRule.Style.Text()) buf.WriteString("\n\n") } if err := gulu.File.WriteFileSafer(custom, buf.Bytes(), 0644); nil != err { util.LogErrorf("write custom css [%s] failed: %s", custom, err) } util.BroadcastByType("main", "refreshtheme", 0, "", map[string]interface{}{ "theme": "/appearance/themes/" + themeName + "/custom.css?" + fmt.Sprintf("%d", time.Now().Unix()), }) return } func keyframes(rule *css.CSSRule, buf *bytes.Buffer) { buf.WriteString(rule.Type.Text()) buf.WriteString(" ") buf.WriteString(rule.Style.Selector.Text()) buf.WriteString(" {\n") for _, r := range rule.Rules { buf.WriteString(r.Style.Text()) buf.WriteString("\n") } buf.WriteString("\n\n") } func fixStyle(style *css.CSSStyleDeclaration) { // css 解析库似乎有 bug,这里做修正 if strings.HasPrefix(style.Property, "-") && !strings.HasPrefix(style.Property, "--") { style.Property = "-" + style.Property } if strings.HasPrefix(style.Value.Text(), "- ") { value := style.Value.Text()[2:] style.Value = css.NewCSSValue(value) } }