Files

80 lines
3.7 KiB
JavaScript

// 临时脚本 v2:意象白名单 + 词边界匹配 + 黑名单过滤,重选候选池
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const ROOT = process.cwd();
const OUT = '_design-test';
fs.mkdirSync(OUT, { recursive: true });
const manifest = JSON.parse(fs.readFileSync('js/data/manifest/icons.json', 'utf8'));
const srcMap = {};
manifest.sources.forEach(s => { srcMap[s.id] = s; });
const TARGETS = ['feather', 'lucide', 'tabler', 'bootstrap', 'remix', 'heroicons', 'iconoir', 'phosphor'];
// 意象白名单:品牌感强的核心词(拆词匹配,词必须独立出现)
const THEMES = {
design: ['pencil', 'pen', 'palette', 'brush', 'paint', 'edit', 'ruler', 'compass', 'pen-tool', 'highlighter', 'marker', 'swatch', 'frame', 'crop', 'layers', 'grid', 'layout', 'design', 'sketch', 'ink', 'draft'],
brand: ['flag', 'tag', 'badge', 'bookmark', 'award', 'crown', 'star', 'sparkle', 'label', 'target', 'certificate', 'ribbon', 'medal', 'trophy'],
symbol: ['hexagon', 'diamond', 'triangle', 'shield', 'anchor', 'gem', 'bolt', 'zap', 'infinity', 'sun', 'moon', 'leaf', 'flower', 'mountain', 'wave', 'snowflake', 'crosshair', 'asterisk'],
tech: ['rocket', 'cpu', 'cloud', 'code', 'terminal', 'wifi', 'database', 'globe', 'chip', 'robot', 'server', 'bug'],
commerce: ['bag', 'cart', 'coin', 'wallet', 'briefcase', 'chart', 'store', 'gift', 'receipt', 'percent'],
misc: ['key', 'lock', 'mail', 'phone', 'calendar', 'clock', 'home', 'book', 'heart', 'camera', 'map', 'plane', 'music', 'mic', 'coffee', 'cup']
};
// 黑名单:语义负面/错误/禁用类,绝不做 LOGO
const BLACK = ['off', 'alert', 'error', 'warning', 'forbidden', 'ban', 'question', 'slash', 'offline', 'disconnected', 'exclamation', 'trash', 'remove', 'delete', 'block', 'stop'];
function words(name) {
return (name || '').toLowerCase().split(/[-_ ]+/).filter(Boolean);
}
function loadSource(sid) {
const file = path.join(ROOT, 'js/data/sources', sid + '.js');
if (!fs.existsSync(file)) return null;
const code = fs.readFileSync(file, 'utf8');
const sb = {};
sb.window = sb; sb.self = sb; sb.globalThis = sb;
vm.runInNewContext(code, sb, { filename: sid + '.js' });
const src = sb.window['HGB_Source_' + sid];
return src ? src.icons : null;
}
const out = [];
const seen = new Set();
for (const sid of TARGETS) {
const cfg = srcMap[sid];
if (!cfg) continue;
const icons = loadSource(sid);
if (!icons) { console.log('SKIP: ' + sid); continue; }
for (const [theme, kws] of Object.entries(THEMES)) {
for (const kw of kws) {
const kwWords = words(kw);
const hits = icons.filter(ic => {
const ws = words(ic.name);
if (!ws.length) return false;
if (ws.some(w => BLACK.includes(w))) return false;
// 白名词须完全包含(词集合 ⊇ 白名词集合)
return kwWords.every(w => ws.includes(w)) && !seen.has(sid + ':' + ic.id);
});
const pick = hits.slice(0, 1); // 每源每意象取 1 个,控制量
for (const h of pick) {
seen.add(sid + ':' + h.id);
out.push({
theme, source: sid, id: h.id, name: h.name,
categoryCn: h.categoryCn || '', type: h.type || cfg.type,
svg: h.svg, license: cfg.license, licenseNote: cfg.licenseNote,
coordText: cfg.coordText, style: cfg.style || ''
});
}
}
}
console.log(` 加载 ${sid} → 命中 ${out.filter(o => o.source === sid).length}`);
}
fs.writeFileSync(path.join(OUT, 'candidates2.json'), JSON.stringify(out, null, 1), 'utf8');
console.log(`\n候选池 v2 共 ${out.length} 个 → candidates2.json`);
const byTheme = {};
out.forEach(o => { byTheme[o.theme] = (byTheme[o.theme] || 0) + 1; });
console.log('主题分布:' + JSON.stringify(byTheme));