package theme import ( "io/fs" "strings" "testing" "github.com/tamakyi/TamaBox/static" ) // DecoSlots 是 --tb-deco / --tb-deco-size / --tb-deco-position 三组令牌 // 必须对齐的槽位数,与 static/css/base.css 里的 12 层背景 // (平铺图案 1 + 装饰 10 + cover 背景图 1)严格对应。 // // 少写一个值不会报错,CSS 会按列表长度循环取值,于是图层与 size/position // 整体错位:典型表现是整页重复同一张插画,或人物跑到页面左上角。 // 这个约定靠注释根本守不住,只能用测试钉死。 const DecoSlots = 10 // TestDecoSlotCount 逐个检查 base.css 与所有主题 css 里每一处装饰层声明。 // 主题可按页面作用域(html[data-page-scope="..."])写多组,所以是「每一处」而不是「每文件」。 func TestDecoSlotCount(t *testing.T) { files := []string{"css/base.css"} for _, slug := range discover() { files = append(files, themeCSSDir+slug+".css") } if len(files) < 2 { t.Fatalf("只找到 %d 个 css,discover() 可能没工作", len(files)) } checked := 0 for _, name := range files { b, err := fs.ReadFile(static.FS, name) if err != nil { t.Fatalf("读取 %s: %v", name, err) } // 注释里会出现「--tb-deco 前两个槽位」这类说明,先剥掉再解析 css := commentRe.ReplaceAllString(string(b), "") for _, key := range []string{"--tb-deco:", "--tb-deco-size:", "--tb-deco-position:"} { // 主题可以不写这组令牌(直接沿用 base.css 的默认值),所以「没有」是合法的; // 但只要写了,就必须写满 —— 半套值比不写更危险。 for i, decl := range declarations(css, key) { checked++ if got := len(splitTopLevel(decl)); got != DecoSlots { t.Errorf("%s 第 %d 处 %s 有 %d 个值,应为 %d 个", name, i+1, key, got, DecoSlots) } } } } if checked == 0 { t.Fatal("一处装饰层声明都没解析到,说明 key 拼错了或 commentRe 剥多了") } t.Logf("共校验 %d 处装饰层声明", checked) } // declarations 取出 key 之后到下一个分号之间的原始文本(不含 key 与分号)。 func declarations(css, key string) []string { var out []string for { i := strings.Index(css, key) if i < 0 { return out } rest := css[i+len(key):] j := strings.IndexByte(rest, ';') if j < 0 { return out } out = append(out, rest[:j]) css = rest[j+1:] } } // splitTopLevel 按顶层逗号切分。不能直接 strings.Split: // rgba(63, 96, 138, 0.18) 和 calc(50% - 524px) 内部的逗号不是层分隔符, // 直接切会把一层算成四层(曾据此误判「已经写够 10 个」)。 func splitTopLevel(s string) []string { var ( out []string buf strings.Builder depth int ) flush := func() { if v := strings.TrimSpace(buf.String()); v != "" { out = append(out, v) } buf.Reset() } for _, r := range s { switch r { case '(': depth++ case ')': depth-- case ',': if depth == 0 { flush() continue } } buf.WriteRune(r) } flush() return out }