conductor

CI task system
git clone git://git.finwo.net/app/conductor
Log | Files | Refs | README | LICENSE

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 }