b037486401
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
122 lines
5.1 KiB
JavaScript
122 lines
5.1 KiB
JavaScript
// 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 → camel:sp-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');
|