ansi.ts (2503B)
1 // web/src/util/ansi.ts - terminal colour codes into vnodes 2 // 3 // Vnodes, never markup: a log must not be able to become a tag. 4 5 import m from 'mithril'; 6 7 const COLOURS = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white']; 8 9 // Cursor moves, erases, and the carriage return a progress bar leaves. 10 const NOISE = /\u001b\[[0-9;?]*[A-Za-z]|\u001b\][^\u0007]*\u0007|\r(?!\n)/g; 11 12 type Style = { fg?: string; bg?: string; bold?: boolean; faint?: boolean; italic?: boolean; underline?: boolean }; 13 14 function classOf(style: Style): string { 15 const parts: string[] = []; 16 if (style.fg) parts.push(`fg-${style.fg}`); 17 if (style.bg) parts.push(`bg-${style.bg}`); 18 if (style.bold) parts.push('bold'); 19 if (style.faint) parts.push('faint'); 20 if (style.italic) parts.push('italic'); 21 if (style.underline) parts.push('underline'); 22 return parts.join(' '); 23 } 24 25 function apply(style: Style, codes: number[]): Style { 26 let next = { ...style }; 27 for (const code of codes) { 28 if (code === 0) next = {}; 29 else if (code === 1) next.bold = true; 30 else if (code === 2) next.faint = true; 31 else if (code === 3) next.italic = true; 32 else if (code === 4) next.underline = true; 33 else if (code === 22) { next.bold = false; next.faint = false; } 34 else if (code === 23) next.italic = false; 35 else if (code === 24) next.underline = false; 36 else if (code >= 30 && code <= 37) next.fg = COLOURS[code - 30]; 37 else if (code === 39) next.fg = undefined; 38 else if (code >= 40 && code <= 47) next.bg = COLOURS[code - 40]; 39 else if (code === 49) next.bg = undefined; 40 else if (code >= 90 && code <= 97) next.fg = `bright-${COLOURS[code - 90]}`; 41 else if (code >= 100 && code <= 107) next.bg = `bright-${COLOURS[code - 100]}`; 42 } 43 return next; 44 } 45 46 export function ansi(text: string): m.Children { 47 const out: m.Children[] = []; 48 let style: Style = {}; 49 let at = 0; 50 51 const sgr = /\u001b\[([0-9;]*)m/g; 52 let match = sgr.exec(text); 53 54 const push = (chunk: string) => { 55 if (!chunk) return; 56 const clean = chunk.replace(NOISE, ''); 57 if (!clean) return; 58 const name = classOf(style); 59 out.push(name ? m('span', { class: name }, clean) : clean); 60 }; 61 62 while (match !== null) { 63 push(text.slice(at, match.index)); 64 const codes = match[1].split(';').filter(Boolean).map(Number); 65 style = apply(style, codes.length > 0 ? codes : [0]); 66 at = match.index + match[0].length; 67 match = sgr.exec(text); 68 } 69 push(text.slice(at)); 70 71 return out; 72 }