feat(theme): 主题支持按页面分别配装饰,生成器改用真实页面画布
原来的生成器只能配「背景 + 人物 + 道具」三件套,预览还是自己画的仿制页面 —— 跟真实页面不像,也没法给不同页面配不同内容。 机制层: - internal/context.pageScope() 按路径归类,输出 <html data-page-scope>: box / question / admin / profile / vote(首页与 /explore 不加载主题,不列) - /user/questions 单独归为 admin:题目列表信息密,和设置表单的装饰诉求不同 - base.css 装饰槽位 6 -> 10,背景 8 层 -> 12 层;主题按作用域覆盖时同样写满 10 个 - 新增 TestDecoSlotCount:逐文件逐声明检查 10 个值。少写一个不报错, 只会让图层与 size/position 错位(整页重复同一张图 / 人物跑到左上角),注释守不住 生成器: - 画板改成 iframe 加载 outputs/preview-*.html(真实模板渲染),配 serve.py 起本地服务 解决 file:// 下父页面读不到 iframe DOM 的问题 - 八个页面作用域(去掉不生效的首页/探索),每个可独立配背景与最多 10 层装饰 - 图层类型:图片 / 径向光斑 / 线性光;9 宫格锚点 + 像素偏移,可在画板上直接拖 - 位置统一写 calc(0%|50%|100% ± Npx):百分比是相对 (容器-图片), 不用 left/bottom 关键字混搭,避免三值语法非法导致整条声明作废 - 配色只填 6 个基础色,其余 4 个必需令牌按方向派生(surface-2 往 text 靠, 浅色变深、深色变亮,方向自动正确) 配套: - zz_preview_test.go 增加单题页 / 题目列表 / 投票页预览,作为生成器画板 - docs/theme-page-scope.md 记录作用域取值、选择器特异性与 10 槽位约束 - StaticVersion 20261020e(base.css 变了) 验证:生成器导出的 CSS 通过 TestThemeCSSComplete + TestDecoSlotCount; 真实箱主页计算样式为 12 层对齐;临时主题验证「question 覆盖为 none」生效、 未覆盖时回退全局。
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user