You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

49 lines
1.9 KiB
TypeScript

// ── ANSI terminal renderer ────────────────────────────────────────────────────
const ANSI_COLOR: Record<string, string> = {
'30': '#4a4a4a', '31': 'var(--red)', '32': 'var(--green)',
'33': 'var(--gold)', '34': '#60a5fa', '35': '#c084fc',
'36': 'var(--cyan)', '37': '#e5e7eb',
'90': '#6b7280', '91': '#f87171', '92': '#4ade80',
'93': '#facc15', '94': '#818cf8', '95': '#e879f9',
'96': '#22d3ee', '97': '#f9fafb',
};
export function ansiToHtml(text: string): string {
let html = '';
let open = false;
for (const chunk of text.split(/(\x1b\[[0-9;]*m)/)) {
const m = chunk.match(/^\x1b\[([0-9;]*)m$/);
if (m) {
if (open) { html += '</span>'; open = false; }
const codes = m[1].split(';');
for (const c of codes) {
if (c === '' || c === '0') break;
if (c === '1') { html += '<span style="font-weight:700">'; open = true; break; }
if (c === '2') { html += '<span style="opacity:0.55">'; open = true; break; }
if (ANSI_COLOR[c]) { html += `<span style="color:${ANSI_COLOR[c]}"`; open = true; break; }
}
if (open && !html.endsWith('>')) html += '>';
} else if (chunk) {
html += chunk.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
}
if (open) html += '</span>';
return html;
}
export function fmt(n: number | null | undefined, decimals = 1, suffix = ''): string {
if (n == null) return '—';
return n.toFixed(decimals) + suffix;
}
export function fmtPct(n: number | null | undefined, decimals = 1): string {
if (n == null) return '—';
return `${n.toFixed(decimals)}%`;
}
export function fmtDate(s: string | null | undefined): string {
if (!s) return '—';
return new Date(s).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: '2-digit' });
}