- match-tokens: alias 增加值条件映射,支持 doctor/patient variant 自动检测 - parse-prototype: 支持 IosFrame label prop 提取(JSX 属性模式) - interaction-rules: login-cta 增加 exclude_patterns 避免"立即关注"误报 - tokens.yml: T.pri/T.priL/T.priD alias 改为数组格式,按值匹配 variant 验证: 患者端 T.pri→--tk-pri 确认,医生端 T.pri→--tk-pri.doctor 确认
707 lines
20 KiB
JavaScript
707 lines
20 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
/**
|
||
* match-tokens.mjs — 三层 Token 匹配
|
||
*
|
||
* 接收 parse-prototype.mjs 的输出 JSON 和 tokens.yml 配置,
|
||
* 对每个原型 Token(别名 / 内联样式值)执行三层匹配,
|
||
* 输出映射关系到 stdout。
|
||
*
|
||
* 三层匹配算法:
|
||
* Layer 1: 别名直查(aliases.prototype_keys)
|
||
* Layer 2: 值精确匹配(带 CSS 属性上下文消歧)
|
||
* Layer 3: 色彩模糊匹配(RGB 欧几里得距离 < 30)
|
||
*
|
||
* 用法:
|
||
* node match-tokens.mjs <parse-result.json> <tokens.yml>
|
||
*/
|
||
|
||
import yaml from 'js-yaml';
|
||
import { readFileSync } from 'fs';
|
||
import { resolve } from 'path';
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 工具函数
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* 解析 hex 颜色字符串为 {r, g, b}
|
||
* 支持 #RGB / #RRGGBB / RRGGBB
|
||
*/
|
||
function hexToRgb(hex) {
|
||
if (!hex || typeof hex !== 'string') return null;
|
||
const cleaned = hex.replace(/^#/, '').trim();
|
||
if (cleaned.length === 3) {
|
||
const r = parseInt(cleaned[0] + cleaned[0], 16);
|
||
const g = parseInt(cleaned[1] + cleaned[1], 16);
|
||
const b = parseInt(cleaned[2] + cleaned[2], 16);
|
||
return { r, g, b };
|
||
}
|
||
if (cleaned.length === 6) {
|
||
const r = parseInt(cleaned.substring(0, 2), 16);
|
||
const g = parseInt(cleaned.substring(2, 4), 16);
|
||
const b = parseInt(cleaned.substring(4, 6), 16);
|
||
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return null;
|
||
return { r, g, b };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* RGB 欧几里得距离
|
||
*/
|
||
function colorDistance(rgb1, rgb2) {
|
||
return Math.sqrt(
|
||
(rgb1.r - rgb2.r) ** 2 +
|
||
(rgb1.g - rgb2.g) ** 2 +
|
||
(rgb1.b - rgb2.b) ** 2,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 判断字符串是否为颜色值(hex 格式)
|
||
*/
|
||
function isHexColor(value) {
|
||
if (!value || typeof value !== 'string') return false;
|
||
return /^#?[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/.test(value.trim());
|
||
}
|
||
|
||
/**
|
||
* 归一化数值:去掉 px 后缀,返回数值字符串(保留小数)
|
||
*/
|
||
function normalizeNumericValue(value) {
|
||
if (value == null) return null;
|
||
const str = String(value).trim();
|
||
if (str.endsWith('px')) {
|
||
return str.slice(0, -2).trim();
|
||
}
|
||
if (str.endsWith('rem') || str.endsWith('em')) {
|
||
return null; // rem/em 标记为 pending,不参与数值匹配
|
||
}
|
||
return str;
|
||
}
|
||
|
||
/**
|
||
* 归一化颜色值:统一为小写 #rrggbb
|
||
*/
|
||
function normalizeColor(value) {
|
||
if (!value) return null;
|
||
const str = String(value).trim();
|
||
const hex = str.startsWith('#') ? str : `#${str}`;
|
||
const rgb = hexToRgb(hex);
|
||
if (!rgb) return null;
|
||
return `#${rgb.r.toString(16).padStart(2, '0')}${rgb.g.toString(16).padStart(2, '0')}${rgb.b.toString(16).padStart(2, '0')}`;
|
||
}
|
||
|
||
/**
|
||
* CSS 属性到 token 类别的映射
|
||
*/
|
||
const CSS_PROPERTY_CATEGORIES = {
|
||
// 颜色类
|
||
color: 'color',
|
||
backgroundColor: 'color',
|
||
'background-color': 'color',
|
||
background: 'color',
|
||
borderColor: 'color',
|
||
'border-color': 'color',
|
||
borderTopColor: 'color',
|
||
borderBottomColor: 'color',
|
||
borderLeftColor: 'color',
|
||
borderRightColor: 'color',
|
||
outlineColor: 'color',
|
||
textColor: 'color',
|
||
fill: 'color',
|
||
|
||
// 间距类
|
||
padding: 'spacing',
|
||
paddingTop: 'spacing',
|
||
paddingBottom: 'spacing',
|
||
paddingLeft: 'spacing',
|
||
paddingRight: 'spacing',
|
||
paddingVertical: 'spacing',
|
||
paddingHorizontal: 'spacing',
|
||
margin: 'spacing',
|
||
marginTop: 'spacing',
|
||
marginBottom: 'spacing',
|
||
marginLeft: 'spacing',
|
||
marginRight: 'spacing',
|
||
gap: 'spacing',
|
||
rowGap: 'spacing',
|
||
columnGap: 'spacing',
|
||
top: 'spacing',
|
||
bottom: 'spacing',
|
||
left: 'spacing',
|
||
right: 'spacing',
|
||
|
||
// 排版类
|
||
fontSize: 'typography',
|
||
lineHeight: 'typography',
|
||
fontWeight: 'typography',
|
||
letterSpacing: 'typography',
|
||
|
||
// 圆角类
|
||
borderRadius: 'radius',
|
||
borderTopLeftRadius: 'radius',
|
||
borderTopRightRadius: 'radius',
|
||
borderBottomLeftRadius: 'radius',
|
||
borderBottomRightRadius: 'radius',
|
||
|
||
// 尺寸类
|
||
width: 'sizing',
|
||
height: 'sizing',
|
||
minWidth: 'sizing',
|
||
minHeight: 'sizing',
|
||
maxWidth: 'sizing',
|
||
maxHeight: 'sizing',
|
||
};
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Token 注册表构建
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* 从 tokens.yml 构建可查找的 Token 列表
|
||
* 返回 { all: [...], byCategory: { color: [...], spacing: [...], ... } }
|
||
*/
|
||
function buildTokenRegistry(tokensConfig) {
|
||
const all = [];
|
||
const byCategory = {};
|
||
|
||
function addToken(entry, category) {
|
||
const record = {
|
||
token: entry.token || null,
|
||
scssVar: entry.scss_var || null,
|
||
value: entry.value || null,
|
||
category,
|
||
role: entry.role || null,
|
||
note: entry.note || null,
|
||
};
|
||
all.push(record);
|
||
if (!byCategory[category]) byCategory[category] = [];
|
||
byCategory[category].push(record);
|
||
}
|
||
|
||
// colors
|
||
if (Array.isArray(tokensConfig.colors)) {
|
||
for (const c of tokensConfig.colors) {
|
||
addToken(c, 'color');
|
||
}
|
||
}
|
||
|
||
// unmapped_scss_variables(颜色类)
|
||
if (Array.isArray(tokensConfig.unmapped_scss_variables)) {
|
||
for (const v of tokensConfig.unmapped_scss_variables) {
|
||
addToken(v, 'color');
|
||
}
|
||
}
|
||
|
||
// typography
|
||
if (Array.isArray(tokensConfig.typography)) {
|
||
for (const t of tokensConfig.typography) {
|
||
addToken(t, 'typography');
|
||
}
|
||
}
|
||
|
||
// structure (line-height 归入 typography)
|
||
if (Array.isArray(tokensConfig.structure)) {
|
||
for (const s of tokensConfig.structure) {
|
||
addToken(s, 'typography');
|
||
}
|
||
}
|
||
|
||
// spacing
|
||
if (Array.isArray(tokensConfig.spacing)) {
|
||
for (const s of tokensConfig.spacing) {
|
||
addToken(s, 'spacing');
|
||
}
|
||
}
|
||
|
||
// radius
|
||
if (Array.isArray(tokensConfig.radius)) {
|
||
for (const r of tokensConfig.radius) {
|
||
if (r && typeof r === 'object') {
|
||
addToken(r, 'radius');
|
||
}
|
||
}
|
||
}
|
||
if (Array.isArray(tokensConfig.radius_unmapped)) {
|
||
for (const r of tokensConfig.radius_unmapped) {
|
||
if (r && typeof r === 'object') {
|
||
addToken(r, 'radius');
|
||
}
|
||
}
|
||
}
|
||
|
||
// sizing
|
||
if (Array.isArray(tokensConfig.sizing)) {
|
||
for (const s of tokensConfig.sizing) {
|
||
addToken(s, 'sizing');
|
||
}
|
||
}
|
||
|
||
// feedback(归入 sizing 或单独)
|
||
if (Array.isArray(tokensConfig.feedback)) {
|
||
for (const f of tokensConfig.feedback) {
|
||
addToken(f, 'feedback');
|
||
}
|
||
}
|
||
|
||
// tag
|
||
if (Array.isArray(tokensConfig.tag)) {
|
||
for (const t of tokensConfig.tag) {
|
||
addToken(t, 'tag');
|
||
}
|
||
}
|
||
|
||
// shadow_unmapped
|
||
if (Array.isArray(tokensConfig.shadow_unmapped)) {
|
||
for (const s of tokensConfig.shadow_unmapped) {
|
||
addToken(s, 'shadow');
|
||
}
|
||
}
|
||
|
||
return { all, byCategory };
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 三层匹配
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Layer 1: 别名直查(支持值条件映射)
|
||
*
|
||
* alias 格式有两种:
|
||
* 单条: { token: "...", status: "exact_match" }
|
||
* 数组: [{ value: "#C4623A", token: "--tk-pri", variant: "patient" },
|
||
* { value: "#3A6B8C", token: "--tk-pri.doctor", variant: "doctor" }]
|
||
*
|
||
* 数组格式按值匹配,单条格式直接匹配(兼容旧格式)。
|
||
*/
|
||
function matchByAlias(key, prototypeValue, aliases) {
|
||
if (!aliases || !aliases.prototype_keys) return null;
|
||
const alias = aliases.prototype_keys[key];
|
||
if (!alias) return null;
|
||
|
||
// 数组格式:按值条件匹配
|
||
if (Array.isArray(alias)) {
|
||
const normalizedInput = normalizeColor(prototypeValue) || normalizeNumericValue(prototypeValue) || String(prototypeValue);
|
||
for (const entry of alias) {
|
||
const normalizedAlias = normalizeColor(entry.value) || normalizeNumericValue(entry.value) || String(entry.value);
|
||
if (normalizedInput === normalizedAlias) {
|
||
const result = {
|
||
method: 'alias',
|
||
confidence: entry.status === 'exact_match' ? 'confirmed' : 'pending',
|
||
token: entry.token || null,
|
||
scssVar: entry.scss_var || null,
|
||
};
|
||
if (entry.variant) result.variant = entry.variant;
|
||
if (entry.note) result.note = entry.note;
|
||
return result;
|
||
}
|
||
}
|
||
// 数组中无值匹配 → 降级到 Layer 2
|
||
return null;
|
||
}
|
||
|
||
// 单条格式(兼容旧格式)
|
||
const result = { method: 'alias', confidence: null };
|
||
|
||
// 值校验:如果 alias 有 value 字段,比较值
|
||
if (alias.value && prototypeValue != null) {
|
||
const normAlias = normalizeColor(alias.value) || normalizeNumericValue(alias.value) || String(alias.value);
|
||
const normProto = normalizeColor(prototypeValue) || normalizeNumericValue(prototypeValue) || String(prototypeValue);
|
||
if (normAlias !== normProto) {
|
||
// 值不匹配 → 降级到 Layer 2
|
||
return null;
|
||
}
|
||
}
|
||
|
||
if (alias.status === 'exact_match') {
|
||
result.token = alias.token || null;
|
||
result.scssVar = alias.scss_var || null;
|
||
result.confidence = 'confirmed';
|
||
} else if (alias.status === 'unmatched') {
|
||
result.token = alias.token || null;
|
||
result.scssVar = alias.scss_var || null;
|
||
result.tokenValue = alias.value || null;
|
||
result.confidence = 'pending';
|
||
if (alias.note) result.note = alias.note;
|
||
if (!result.token && alias.scssVar) {
|
||
result.note = result.note || `tokens.scss 未声明为 CSS 变量,直接使用 SCSS 变量 ${alias.scssVar}`;
|
||
}
|
||
} else if (alias.status === 'approximate') {
|
||
result.token = alias.token || null;
|
||
result.scssVar = alias.scss_var || null;
|
||
result.confidence = 'approximate';
|
||
if (alias.note) result.note = alias.note;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 获取用于值匹配的候选 token 列表
|
||
* 根据 CSS 属性确定查找类别
|
||
*/
|
||
function getCandidatesByProperty(propertyName, registry) {
|
||
const category = CSS_PROPERTY_CATEGORIES[propertyName];
|
||
|
||
if (!category) {
|
||
// 未知属性,在所有 token 中搜索
|
||
return registry.all;
|
||
}
|
||
|
||
if (category === 'color') {
|
||
// color 类同时搜索 colors + unmapped_scss_variables
|
||
return [
|
||
...(registry.byCategory.color || []),
|
||
...(registry.byCategory.shadow || []), // 阴影也含颜色信息
|
||
];
|
||
}
|
||
|
||
if (category === 'spacing') {
|
||
return [
|
||
...(registry.byCategory.spacing || []),
|
||
...(registry.byCategory.tag || []), // tag 也有 padding
|
||
];
|
||
}
|
||
|
||
if (category === 'typography') {
|
||
return registry.byCategory.typography || [];
|
||
}
|
||
|
||
if (category === 'radius') {
|
||
return registry.byCategory.radius || [];
|
||
}
|
||
|
||
if (category === 'sizing') {
|
||
return [
|
||
...(registry.byCategory.sizing || []),
|
||
...(registry.byCategory.feedback || []),
|
||
];
|
||
}
|
||
|
||
return registry.byCategory[category] || registry.all;
|
||
}
|
||
|
||
/**
|
||
* Layer 2: 值精确匹配(带 CSS 属性上下文消歧)
|
||
* @param {string} value - 原型中的值
|
||
* @param {string|null} cssProperty - 关联的 CSS 属性名(用于消歧)
|
||
* @param {object} registry - Token 注册表
|
||
* @returns {object|null} 匹配结果
|
||
*/
|
||
function matchByValue(value, cssProperty, registry) {
|
||
if (value == null) return null;
|
||
|
||
const candidates = getCandidatesByProperty(cssProperty, registry);
|
||
if (candidates.length === 0) return null;
|
||
|
||
// 颜色匹配
|
||
if (isHexColor(value)) {
|
||
const normalizedInput = normalizeColor(value);
|
||
if (!normalizedInput) return null;
|
||
|
||
for (const candidate of candidates) {
|
||
if (!candidate.value) continue;
|
||
if (!isHexColor(candidate.value)) continue;
|
||
const normalizedCandidate = normalizeColor(candidate.value);
|
||
if (normalizedCandidate === normalizedInput) {
|
||
return buildValueMatchResult(candidate, value);
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// 数值匹配(含 px 后缀)
|
||
const normalizedInput = normalizeNumericValue(value);
|
||
if (normalizedInput === null) {
|
||
// rem/em — 标记为 pending
|
||
return {
|
||
token: null,
|
||
prototypeValue: value,
|
||
method: 'value_exact',
|
||
confidence: 'pending',
|
||
note: 'rem/em 值无法自动匹配,需手动确认',
|
||
};
|
||
}
|
||
|
||
for (const candidate of candidates) {
|
||
if (!candidate.value) continue;
|
||
const normalizedCandidate = normalizeNumericValue(candidate.value);
|
||
if (normalizedCandidate === null) continue;
|
||
if (normalizedCandidate === normalizedInput) {
|
||
return buildValueMatchResult(candidate, value);
|
||
}
|
||
}
|
||
|
||
// 无单位数值直接比较(如 lineHeight: 1.5)
|
||
for (const candidate of candidates) {
|
||
if (!candidate.value) continue;
|
||
if (String(candidate.value).trim() === String(value).trim()) {
|
||
return buildValueMatchResult(candidate, value);
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 构建值匹配结果
|
||
*/
|
||
function buildValueMatchResult(candidate, prototypeValue) {
|
||
const result = {
|
||
token: candidate.token || null,
|
||
scssVar: candidate.scssVar || null,
|
||
prototypeValue,
|
||
tokenValue: candidate.value,
|
||
method: 'value_exact',
|
||
confidence: candidate.token ? 'confirmed' : 'pending',
|
||
};
|
||
if (candidate.role) result.role = candidate.role;
|
||
if (!candidate.token && candidate.scssVar) {
|
||
result.note = `匹配到 SCSS 变量 ${candidate.scssVar},但无对应 CSS Token`;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Layer 3: 色彩模糊匹配(RGB 欧几里得距离)
|
||
* @param {string} value - hex 颜色值
|
||
* @param {object} registry - Token 注册表
|
||
* @param {number} threshold - 距离阈值,默认 30
|
||
* @returns {object|null} 最佳近似匹配
|
||
*/
|
||
function matchByColorFuzzy(value, registry, threshold = 30) {
|
||
const inputRgb = hexToRgb(value);
|
||
if (!inputRgb) return null;
|
||
|
||
// 在所有颜色类 token 中搜索
|
||
const colorCandidates = [
|
||
...(registry.byCategory.color || []),
|
||
];
|
||
|
||
let bestMatch = null;
|
||
let bestDistance = Infinity;
|
||
|
||
for (const candidate of colorCandidates) {
|
||
if (!candidate.value) continue;
|
||
const candidateRgb = hexToRgb(candidate.value);
|
||
if (!candidateRgb) continue;
|
||
|
||
const dist = colorDistance(inputRgb, candidateRgb);
|
||
if (dist < bestDistance) {
|
||
bestDistance = dist;
|
||
bestMatch = candidate;
|
||
}
|
||
}
|
||
|
||
if (bestMatch && bestDistance < threshold) {
|
||
return {
|
||
token: bestMatch.token || null,
|
||
scssVar: bestMatch.scssVar || null,
|
||
prototypeValue: value,
|
||
tokenValue: bestMatch.value,
|
||
method: 'color_fuzzy',
|
||
confidence: 'approximate',
|
||
distance: Math.round(bestDistance * 100) / 100,
|
||
note: `颜色近似匹配(RGB 距离 ${Math.round(bestDistance * 100) / 100})`,
|
||
};
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 对单个值执行完整三层匹配
|
||
* @param {string} value - 要匹配的值
|
||
* @param {string|null} cssProperty - CSS 属性名(消歧用)
|
||
* @param {object} registry - Token 注册表
|
||
* @returns {object} 匹配结果
|
||
*/
|
||
function fullMatchValue(value, cssProperty, registry) {
|
||
// Layer 2: 值精确匹配
|
||
const exact = matchByValue(value, cssProperty, registry);
|
||
if (exact) return exact;
|
||
|
||
// Layer 3: 颜色模糊匹配(仅对颜色值)
|
||
if (isHexColor(value)) {
|
||
const fuzzy = matchByColorFuzzy(value, registry);
|
||
if (fuzzy) return fuzzy;
|
||
}
|
||
|
||
// 未匹配
|
||
return {
|
||
token: null,
|
||
scssVar: null,
|
||
prototypeValue: value,
|
||
method: 'none',
|
||
confidence: 'unmatched',
|
||
};
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 主流程
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function main() {
|
||
const args = process.argv.slice(2);
|
||
|
||
if (args.length < 2) {
|
||
process.stderr.write('用法: node match-tokens.mjs <parse-result.json> <tokens.yml>\n');
|
||
process.exit(1);
|
||
}
|
||
|
||
const [parseResultPath, tokensYmlPath] = args;
|
||
|
||
// 读取输入文件
|
||
let parseResult;
|
||
try {
|
||
const raw = readFileSync(resolve(parseResultPath), 'utf-8');
|
||
parseResult = JSON.parse(raw);
|
||
} catch (err) {
|
||
process.stderr.write(`无法读取/解析 parse-result JSON: ${err.message}\n`);
|
||
process.exit(1);
|
||
}
|
||
|
||
let tokensConfig;
|
||
try {
|
||
const raw = readFileSync(resolve(tokensYmlPath), 'utf-8');
|
||
tokensConfig = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
|
||
} catch (err) {
|
||
process.stderr.write(`无法读取/解析 tokens.yml: ${err.message}\n`);
|
||
process.exit(1);
|
||
}
|
||
|
||
// 构建 Token 注册表
|
||
const registry = buildTokenRegistry(tokensConfig);
|
||
const aliases = tokensConfig.aliases || null;
|
||
|
||
// ---- 匹配原型 Token 别名 ----
|
||
const matched = {};
|
||
const unmatched = [];
|
||
|
||
const prototypeTokens = parseResult.tokens || {};
|
||
for (const [key, value] of Object.entries(prototypeTokens)) {
|
||
// Layer 1: 别名直查
|
||
const aliasResult = matchByAlias(key, value, aliases);
|
||
if (aliasResult) {
|
||
matched[key] = {
|
||
...aliasResult,
|
||
prototypeValue: value,
|
||
// 如果 alias 没有覆盖 tokenValue,从 token 注册表中查找
|
||
tokenValue: aliasResult.tokenValue || null,
|
||
};
|
||
// 补充 tokenValue
|
||
if (aliasResult.token && !matched[key].tokenValue) {
|
||
const found = registry.all.find(t => t.token === aliasResult.token);
|
||
if (found) matched[key].tokenValue = found.value;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// 无别名,尝试值匹配(需要推断 CSS 属性上下文)
|
||
const inferredProperty = inferPropertyFromTokenKey(key);
|
||
const valueResult = fullMatchValue(value, inferredProperty, registry);
|
||
if (valueResult.confidence !== 'unmatched') {
|
||
matched[key] = valueResult;
|
||
} else {
|
||
unmatched.push(key);
|
||
}
|
||
}
|
||
|
||
// ---- 匹配内联样式值 ----
|
||
const inlineTokenMap = {};
|
||
const inlineStyles = parseResult.inlineStyles || {};
|
||
|
||
for (const [cssProperty, values] of Object.entries(inlineStyles)) {
|
||
for (const rawValue of values) {
|
||
const mapKey = `${cssProperty}:${rawValue}`;
|
||
const result = fullMatchValue(rawValue, cssProperty, registry);
|
||
if (result.confidence !== 'unmatched') {
|
||
inlineTokenMap[mapKey] = {
|
||
token: result.token,
|
||
scssVar: result.scssVar || undefined,
|
||
tokenValue: result.tokenValue || undefined,
|
||
confidence: result.confidence,
|
||
method: result.method,
|
||
};
|
||
if (result.note) inlineTokenMap[mapKey].note = result.note;
|
||
if (result.distance != null) inlineTokenMap[mapKey].distance = result.distance;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- 汇总统计 ----
|
||
let confirmed = 0;
|
||
let pending = 0;
|
||
let approximate = 0;
|
||
|
||
for (const entry of Object.values(matched)) {
|
||
if (entry.confidence === 'confirmed') confirmed++;
|
||
else if (entry.confidence === 'pending') pending++;
|
||
else if (entry.confidence === 'approximate') approximate++;
|
||
}
|
||
|
||
for (const entry of Object.values(inlineTokenMap)) {
|
||
if (entry.confidence === 'confirmed') confirmed++;
|
||
else if (entry.confidence === 'pending') pending++;
|
||
else if (entry.confidence === 'approximate') approximate++;
|
||
}
|
||
|
||
const totalAlias = Object.keys(prototypeTokens).length;
|
||
const totalInline = Object.values(inlineStyles).reduce((sum, arr) => sum + arr.length, 0);
|
||
|
||
// 检测 variant
|
||
const variantEntries = Object.values(matched).filter(e => e.variant);
|
||
const variant = variantEntries.length > 0 ? variantEntries[0].variant : null;
|
||
|
||
// ---- 输出 ----
|
||
const output = {
|
||
source: parseResult.source || null,
|
||
variant,
|
||
matched,
|
||
unmatched,
|
||
inlineTokenMap,
|
||
summary: {
|
||
total: totalAlias + totalInline,
|
||
aliasTokens: totalAlias,
|
||
inlineValues: totalInline,
|
||
confirmed,
|
||
pending,
|
||
approximate,
|
||
unmatched: unmatched.length,
|
||
},
|
||
};
|
||
|
||
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
||
}
|
||
|
||
/**
|
||
* 从 Token 别名 key 推断 CSS 属性(用于无别名时的消歧)
|
||
*/
|
||
function inferPropertyFromTokenKey(key) {
|
||
const lower = key.toLowerCase();
|
||
|
||
if (lower.includes('pri') || lower.includes('color') || lower.includes('bg') ||
|
||
lower.includes('tx') || lower.includes('bd') || lower.includes('card') ||
|
||
lower.includes('surface') || lower.includes('acc') || lower.includes('dan') ||
|
||
lower.includes('wrn') || lower.includes('wechat')) {
|
||
return 'color';
|
||
}
|
||
if (lower.includes('font') || lower.includes('text') && !lower.includes('color')) {
|
||
return 'fontSize';
|
||
}
|
||
if (lower.includes('r') && (lower.includes('sm') || lower.includes('xs') || lower.length <= 3)) {
|
||
return 'borderRadius';
|
||
}
|
||
if (lower.includes('gap') || lower.includes('pad') || lower.includes('margin')) {
|
||
return 'padding';
|
||
}
|
||
|
||
return null; // 未知,不限制类别
|
||
}
|
||
|
||
// 启动
|
||
main();
|