Files
drawlogo/scripts/audit-blank.js
T

218 lines
9.2 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.
/**
* 全量审计:找出渲染后为空白/不可见的图标
* 检查项(对每个源每个图标):
* 1. 无几何内容(body 里没有任何可渲染元素)
* 2. 几何完全越界(所有坐标在 24 视口外,会被裁剪成空白)
* 3. 所有几何元素均不可见(fill=none 无 stroke / opacity=0 / visibility=hidden / display=none,
* 考虑 <g> 祖先的 fill/stroke/opacity 继承)
* 用法:node scripts/audit-blank.js [源id过滤,逗号分隔]
*/
const fs = require('fs');
const path = require('path');
const SRC_DIR = path.join(process.cwd(), 'js/data/sources');
const only = process.argv[2] ? process.argv[2].split(',').map(s => s.trim()) : null;
const GEOM = ['path', 'circle', 'rect', 'ellipse', 'line', 'polyline', 'polygon', 'text', 'image', 'use', 'textPath'];
/** 与 js/ui/panels.js 的 STROKE_SOURCES 一致:描边风来源(构建时 stroke 被剥离只剩 path d) */
const STROKE_SOURCES = new Set(['feather', 'lucide', 'tabler']);
function load(sid) {
const txt = fs.readFileSync(path.join(SRC_DIR, sid + '.js'), 'utf8');
global.window = global.window || {};
eval(txt);
return global.window['HGB_Source_' + sid];
}
/** 轻量标签解析:返回元素树,含 parent 指针 */
function parseBody(body) {
const roots = [];
const stack = [];
const re = /<(\/?)([a-zA-Z][\w-]*)([^>]*?)(\/?)>/g;
let m;
while ((m = re.exec(body))) {
const closing = m[1] === '/';
const tag = m[2].toLowerCase();
const attrsStr = m[3];
const selfClose = m[4] === '/' || tag === 'style' || tag === 'defs' || tag === 'title' || tag === 'desc';
if (closing) { stack.pop(); continue; }
const attrs = {};
const am = attrsStr.matchAll(/([\w:-]+)\s*=\s*"([^"]*)"/g);
for (const a of am) attrs[a[1]] = a[2];
const node = { tag, attrs, children: [], parent: stack.length ? stack[stack.length - 1] : null };
if (stack.length) stack[stack.length - 1].children.push(node);
else roots.push(node);
if (!selfClose) stack.push(node);
}
return roots;
}
/** 收集节点自身的最近有效属性(含祖先继承) */
function effAttrs(node) {
const e = {};
for (let n = node; n; n = n.parent) {
if (e.fill === undefined && n.attrs.fill !== undefined) e.fill = n.attrs.fill;
if (e.stroke === undefined && n.attrs.stroke !== undefined) e.stroke = n.attrs.stroke;
if (e['stroke-width'] === undefined && n.attrs['stroke-width'] !== undefined) e['stroke-width'] = n.attrs['stroke-width'];
if (e.opacity === undefined && n.attrs.opacity !== undefined) e.opacity = n.attrs.opacity;
if (e.visibility === undefined && n.attrs.visibility !== undefined) e.visibility = n.attrs.visibility;
if (e.display === undefined && n.attrs.display !== undefined) e.display = n.attrs.display;
}
return e;
}
/** 单个几何元素是否可见(纯静态,无 svg 根属性模拟) */
function nodeVisible(node) {
const e = effAttrs(node);
if (e.visibility === 'hidden' || e.visibility === 'collapse') return false;
if (e.display === 'none') return false;
if (e.opacity !== undefined && parseFloat(e.opacity) === 0) return false;
const hasStroke = e.stroke !== undefined && e.stroke !== 'none';
if (e.fill === undefined) return true; // 全链无 fill → svg 默认黑色填充 → 可见
const hasFill = e.fill !== 'none' && e.fill !== 'transparent'; // url(#…) 引用也算可渲染
return hasStroke || hasFill;
}
/** 收集 body 中所有几何元素 */
function collectGeoms(body) {
const geoms = [];
(function walk(ns) { for (const n of ns) { if (GEOM.includes(n.tag)) geoms.push(n); walk(n.children); } })(parseBody(body));
return geoms;
}
/**
* 渲染模拟:复刻 js/ui/panels.js renderBuiltinGrid 的判定
* - preserve(emoji/brand 多色)保留原色
* - stroke 型:<svg fill="none" stroke=灰 …>,几何靠描边或自带彩色填充显示
* - fill 型:<svg fill=灰 stroke="none">,几何靠填充显示
* body 预处理:fill/stroke="currentColor" → "inherit"
*/
function renderVisible(geoms, body, sid, type) {
const replaced = body.replace(/fill="currentColor"/g, 'fill="inherit"').replace(/stroke="currentColor"/g, 'stroke="inherit"');
if (type && type !== 'icon') {
// 保留原色:按元素自身属性判定
return geoms.some(nodeVisible);
}
const isStroke = STROKE_SOURCES.has(sid) || /stroke\s*=|\bstroke-linecap\s*=|\bstroke-linejoin\s*=/.test(replaced);
const rootFill = isStroke ? 'none' : '#374151';
const rootStroke = isStroke ? '#374151' : 'none';
return geoms.some(g => {
const e = effAttrs(g);
if (e.visibility === 'hidden' || e.visibility === 'collapse') return false;
if (e.display === 'none') return false;
if (e.opacity !== undefined && parseFloat(e.opacity) === 0) return false;
// currentColor→inherit 已在 replaced 中体现,但 effAttrs 用的是原始 body;这里手动映射
const fill = e.fill === undefined ? rootFill : (e.fill === 'currentColor' ? 'inherit' : e.fill);
const stroke = e.stroke === undefined ? rootStroke : (e.stroke === 'currentColor' ? 'inherit' : e.stroke);
const hasStroke = stroke !== undefined && stroke !== 'none' && stroke !== 'inherit' || (stroke === 'inherit' && rootStroke !== 'none');
const hasFill = fill !== 'none' && fill !== 'transparent' && fill !== 'inherit' || (fill === 'inherit' && rootFill !== 'none');
return hasStroke || hasFill;
});
}
/** 提取 path d 字符串列表 */
function pathDs(body) {
const out = [];
let re = /<path\b[^>]*>/g, m;
while ((m = re.exec(body))) {
const dm = m[0].match(/\bd="([^"]*)"/);
if (dm) out.push(dm[1]);
}
return out;
}
/** 从 body 提取所有数值(path d 坐标 + 几何属性,排除弧段 flags) */
function geomNumbers(body) {
const nums = [];
const dRe = /\bd="([^"]*)"/g;
let dm;
while ((dm = dRe.exec(body))) {
// 按命令分段,跳过弧段 flags(仅 0/1,混入会干扰越界判断)
const d = dm[1];
const cmds = d.match(/[a-zA-Z][^a-zA-Z]*/g) || [];
for (const c of cmds) {
const cmd = c[0];
const vals = (c.slice(1).match(/-?\d*\.?\d+(?:e[+-]?\d+)?/gi) || []).map(parseFloat);
if ((cmd === 'A' || cmd === 'a') && vals.length >= 7) {
const rx = vals.shift(), ry = vals.shift();
nums.push(rx, ry);
vals.shift(); // x-rotation
vals.shift(); vals.shift(); // flags 0/1 不参与
nums.push(...vals);
} else {
nums.push(...vals);
}
}
}
const attrs = /(?:cx|cy|rx|ry|x1|y1|x2|y2|width|height|x|y|r|points)="(-?[\d\s.,eE+-]+)"/g;
let m;
while ((m = attrs.exec(body))) {
const vals = m[1].match(/-?\d*\.?\d+(?:e[+-]?\d+)?/gi) || [];
for (const n of vals) nums.push(parseFloat(n));
}
return nums;
}
function hasGeom(body) {
return GEOM.some(t => new RegExp('<' + t + '\\b').test(body));
}
function checkIcon(ic, sid) {
const body = ic.svg || '';
const reasons = [];
if (!body.trim()) reasons.push('空 body');
if (!hasGeom(body)) { reasons.push('无几何元素'); return reasons; }
const geoms = collectGeoms(body);
// 渲染模拟可见性:复刻 panels.js 渲染判定
const visible = renderVisible(geoms, body, sid, ic.type);
if (!visible) {
const sample = geoms.slice(0, 2).map(g => {
const e = effAttrs(g);
return `${g.tag}{fill:${e.fill},stroke:${e.stroke},opacity:${e.opacity}}`;
}).join(' ');
reasons.push(`渲染后不可见 (${geoms.length}个: ${sample})`);
}
// 越界检查:非 wrap 归一化时,几何完全落在 24 视口外
if (!/transform="translate\([^"]*\)\s*scale\(/.test(body)) {
const nums = geomNumbers(body);
if (nums.length) {
const min = Math.min(...nums), max = Math.max(...nums);
if (max < -1 || min > 25) reasons.push(`几何越界 [${min},${max}]`);
}
}
return reasons;
}
function main() {
const files = fs.readdirSync(SRC_DIR).filter(f => f.endsWith('.js') && !f.startsWith('dicebear-'));
const total = { icons: 0, bad: 0 };
const perSource = [];
for (const f of files) {
const sid = f.replace(/\.js$/, '');
if (only && !only.includes(sid)) continue;
let data;
try { data = load(sid); } catch (e) { perSource.push({ sid, error: '加载失败 ' + e.message }); continue; }
if (!data || !Array.isArray(data.icons)) { perSource.push({ sid, error: '无 icons 数组' }); continue; }
const bad = [];
for (const ic of data.icons) {
total.icons++;
const reasons = checkIcon(ic, sid);
if (reasons.length) {
total.bad++;
bad.push({ id: ic.id || ic.name, reasons, len: (ic.svg || '').length });
}
}
if (bad.length) perSource.push({ sid, count: data.icons.length, bad, ratio: (bad.length / data.icons.length * 100).toFixed(1) + '%' });
}
console.log(`\n扫描 ${total.icons.toLocaleString()} 个图标,发现可疑 ${total.bad.toLocaleString()} 个`);
perSource.filter(s => s.bad).sort((a, b) => b.bad.length - a.bad.length).forEach(s => {
console.log(`\n■ ${s.sid}: ${s.bad.length}/${s.count}(${s.ratio})`);
s.bad.slice(0, 40).forEach(b => console.log(` ${b.id} [${b.reasons.join('; ')}] len=${b.len}`));
if (s.bad.length > 40) console.log(` … 等 ${s.bad.length - 40} 个`);
});
perSource.filter(s => s.error).forEach(s => console.log(`\n✗ ${s.sid}: ${s.error}`));
}
main();