Files
jiu/client/tool/gen_tokens.mjs
T
wangjia b037486401 feat(client): token 地基补字阶/圆角/间距标量层 AppDims(codegen 单源)
gen_tokens.mjs 扩展解析 :root 共享块 → 生成 app_dims.g.dart:
间距 sp1-10 / 圆角 rSm-rPill / 字号 fsDisplay-fsXs,主题无关 const 标量。
为屏幕重建提供「禁硬编码 px」的单一真源;app_tokens.g.dart 保持字节一致。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSKEiHsvauyxYUW2itzUXX
2026-06-25 06:51:52 +08:00

122 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// client/tool/gen_tokens.mjs
// 解析 token_source/tokens.css 的三套主题块 → 生成 app_tokens.g.dart。
// 用法:node tool/gen_tokens.mjs (在 client/ 下运行)
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const css = fs.readFileSync(path.join(ROOT, 'lib/core/theme/token_source/tokens.css'), 'utf8');
// CSS 变量名(kebab) → dart 字段(camel)。仅这些进 AppTokens。
const FIELDS = {
'page':'page','bg':'bg','surface':'surface','border':'border','border-subtle':'borderSubtle',
'text':'text','heading':'heading','title':'title','muted':'muted','faint':'faint',
'primary':'primary','primary-dark':'primaryDark','on-primary':'onPrimary','brand400':'brand400',
'brand50':'brand50','accent':'accent','success':'success','danger':'danger','warn':'warn',
'info-soft':'infoSoft','ok-soft':'okSoft','accent-soft':'accentSoft','success-bg':'successBg',
'danger-bg':'dangerBg','warn-bg':'warnBg','th-bg':'thBg','row-hover':'rowHover','shadow':'shadow',
'top-bg':'topBg','top-fg':'topFg','top-faint':'topFaint','top-border':'topBorder','top-control':'topControl',
'top-mark-bg':'topMarkBg','top-mark-fg':'topMarkFg','side-bg':'sideBg','side-fg':'sideFg',
'side-muted':'sideMuted','side-active-bg':'sideActiveBg','side-active-fg':'sideActiveFg',
'side-border':'sideBorder','side-hover':'sideHover','menu-user-bg':'menuUserBg',
'menu-user-fg':'menuUserFg','menu-user-hover':'menuUserHover',
};
const DARK = { a:false, b:true, c:false };
function block(key){
const m = css.match(new RegExp(`\\[data-theme="${key}"\\]\\s*\\{([^}]*)\\}`));
if(!m) throw new Error('missing theme block: '+key);
const out = {};
for(const mm of m[1].matchAll(/--([a-z0-9-]+)\s*:\s*([^;]+);/g)) out[mm[1]] = mm[2].trim();
return out;
}
function toColor(raw){
let v = raw.trim();
if(v.startsWith('linear-gradient')){ const h = v.match(/#([0-9a-fA-F]{6})/); v = '#'+h[1]; } // 取首个 hex
let m = v.match(/^#([0-9a-fA-F]{6})$/);
if(m) return `Color(0xFF${m[1].toUpperCase()})`;
m = v.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)$/);
if(m){ const a = Math.round((m[4]===undefined?1:parseFloat(m[4]))*255).toString(16).padStart(2,'0');
const hex = [m[1],m[2],m[3]].map(n=>(+n).toString(16).padStart(2,'0')).join(''); return `Color(0x${a}${hex.toUpperCase()})`; }
throw new Error('unsupported color value: '+raw);
}
function emit(key){
const b = block(key);
const lines = Object.entries(FIELDS).map(([css,dart])=>{
if(b[css]===undefined) throw new Error(`theme ${key} missing --${css}`);
return ` ${dart}: ${toColor(b[css])},`;
});
lines.push(` isDark: ${DARK[key]},`);
return `const AppTokens kTokens${key.toUpperCase()} = AppTokens(\n${lines.join('\n')}\n);`;
}
const body = `// GENERATED by tool/gen_tokens.mjs — DO NOT EDIT.
// 源:lib/core/theme/token_source/tokens.css(从原型同步)。改色请改源后重跑脚本。
import 'package:flutter/material.dart';
import 'app_tokens.dart';
${emit('a')}
${emit('b')}
${emit('c')}
AppTokens appTokensOf(String key) {
switch (key) {
case 'b': return kTokensB;
case 'c': return kTokensC;
default: return kTokensA;
}
}
`;
fs.writeFileSync(path.join(ROOT, 'lib/core/theme/app_tokens.g.dart'), body);
console.log('generated app_tokens.g.dart');
// ── 主题无关标量(间距/圆角/字号):解析 :root{} 共享块 → AppDims ──
function rootBlock(){
// 匹配 ":root{" 而非 ":root[data-theme=...]"(前者 { 紧跟)
const m = css.match(/:root\s*\{([^}]*)\}/);
if(!m) throw new Error('missing :root shared block');
const out = {};
for(const mm of m[1].matchAll(/--([a-z0-9-]+)\s*:\s*([^;]+);/g)) out[mm[1]] = mm[2].trim();
return out;
}
// kebab → camelsp-1→sp1 · r-sm→rSm · fs-h1→fsH1 · fs-display→fsDisplay
function camel(name){
const [h, ...rest] = name.split('-');
return h + rest.map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('');
}
function px(raw){
const m = raw.trim().match(/^([\d.]+)px$/);
if(!m) throw new Error('unsupported dim value: ' + raw);
return m[1];
}
function emitDims(){
const r = rootBlock();
const groups = [
['间距', k => k.startsWith('sp-')],
['圆角', k => k.startsWith('r-')],
['字号', k => k.startsWith('fs-')],
];
const lines = [];
for(const [label, pick] of groups){
const keys = Object.keys(r).filter(pick);
if(!keys.length) continue;
lines.push(` // ${label}`);
for(const k of keys) lines.push(` static const double ${camel(k)} = ${px(r[k])};`);
}
return lines.join('\n');
}
const dimsBody = `// GENERATED by tool/gen_tokens.mjs — DO NOT EDIT.
// 源:lib/core/theme/token_source/tokens.css:root 共享块,主题无关)。改尺寸请改源后重跑脚本。
// 间距/圆角/字号三套主题统一,故为 const 标量,不进 per-theme AppTokens。
class AppDims {
AppDims._();
${emitDims()}
}
`;
fs.writeFileSync(path.join(ROOT, 'lib/core/theme/app_dims.g.dart'), dimsBody);
console.log('generated app_dims.g.dart');