github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/web/src/third-party/anser/index.js (about)

     1  "use strict";
     2  
     3  // This file was originally written by @drudru (https://github.com/drudru/ansi_up), MIT, 2011
     4  // Forked from https://github.com/IonicaBizau/anser/blob/ac20b53394a933ac9a2364159f49dd6903f08682/lib/index.js for Tilt Dev.
     5  
     6  const ANSI_COLORS = [
     7      [
     8          { color: "0, 0, 0",        "class": "ansi-black"   }
     9        , { color: "#f6685c",      "class": "ansi-red"     }
    10        , { color: "#20ba31",      "class": "ansi-green"   }
    11        , { color: "#fcb41e",    "class": "ansi-yellow"  }
    12        , { color: "#03c7d3",      "class": "ansi-blue"    }
    13        , { color: "#6378ba",    "class": "ansi-magenta" }
    14        , { color: "#5edbe3",    "class": "ansi-cyan"    }
    15        , { color: "255,255,255",    "class": "ansi-white"   }
    16      ]
    17    , [
    18          { color: "#586e75",     "class": "ansi-bright-black"   }
    19        , { color: "#f7aaa4",    "class": "ansi-bright-red"     }
    20        , { color: "#70d37b",      "class": "ansi-bright-green"   }
    21        , { color: "#fdcf6f",   "class": "ansi-bright-yellow"  }
    22        , { color: "#5edbe3",    "class": "ansi-bright-blue"    }
    23        , { color: "#6378ba",   "class": "ansi-bright-magenta" }
    24        , { color: "85, 255, 255",   "class": "ansi-bright-cyan"    }
    25        , { color: "255, 255, 255",  "class": "ansi-bright-white"   }
    26      ]
    27  ];
    28  
    29  class Anser {
    30  
    31      /**
    32       * Anser.escapeForHtml
    33       * Escape the input HTML.
    34       *
    35       * This does the minimum escaping of text to make it compliant with HTML.
    36       * In particular, the '&','<', and '>' characters are escaped. This should
    37       * be run prior to `ansiToHtml`.
    38       *
    39       * @name Anser.escapeForHtml
    40       * @function
    41       * @param {String} txt The input text (containing the ANSI snippets).
    42       * @returns {String} The escaped html.
    43       */
    44      static escapeForHtml (txt) {
    45          return new Anser().escapeForHtml(txt);
    46      }
    47  
    48      /**
    49       * Anser.linkify
    50       * Adds the links in the HTML.
    51       *
    52       * This replaces any links in the text with anchor tags that display the
    53       * link. The links should have at least one whitespace character
    54       * surrounding it. Also, you should apply this after you have run
    55       * `ansiToHtml` on the text.
    56       *
    57       * @name Anser.linkify
    58       * @function
    59       * @param {String} txt The input text.
    60       * @returns {String} The HTML containing the <a> tags (unescaped).
    61       */
    62      static linkify (txt) {
    63          return new Anser().linkify(txt);
    64      }
    65  
    66      /**
    67       * Anser.ansiToHtml
    68       * This replaces ANSI terminal escape codes with SPAN tags that wrap the
    69       * content.
    70       *
    71       * This function only interprets ANSI SGR (Select Graphic Rendition) codes
    72       * that can be represented in HTML.
    73       * For example, cursor movement codes are ignored and hidden from output.
    74       * The default style uses colors that are very close to the prescribed
    75       * standard. The standard assumes that the text will have a black
    76       * background. These colors are set as inline styles on the SPAN tags.
    77       *
    78       * Another option is to set `use_classes: true` in the options argument.
    79       * This will instead set classes on the spans so the colors can be set via
    80       * CSS. The class names used are of the format `ansi-*-fg/bg` and
    81       * `ansi-bright-*-fg/bg` where `*` is the color name,
    82       * i.e black/red/green/yellow/blue/magenta/cyan/white.
    83       *
    84       * @name Anser.ansiToHtml
    85       * @function
    86       * @param {String} txt The input text.
    87       * @param {Object} options The options passed to the ansiToHTML method.
    88       * @returns {String} The HTML output.
    89       */
    90      static ansiToHtml (txt, options) {
    91          return new Anser().ansiToHtml(txt, options);
    92      }
    93  
    94      /**
    95       * Anser.ansiToJson
    96       * Converts ANSI input into JSON output.
    97       *
    98       * @name Anser.ansiToJson
    99       * @function
   100       * @param {String} txt The input text.
   101       * @param {Object} options The options passed to the ansiToHTML method.
   102       * @returns {String} The HTML output.
   103       */
   104      static ansiToJson (txt, options) {
   105          return new Anser().ansiToJson(txt, options);
   106      }
   107  
   108      /**
   109       * Anser.ansiToText
   110       * Converts ANSI input into text output.
   111       *
   112       * @name Anser.ansiToText
   113       * @function
   114       * @param {String} txt The input text.
   115       * @returns {String} The text output.
   116       */
   117      static ansiToText (txt) {
   118          return new Anser().ansiToText(txt);
   119      }
   120  
   121      /**
   122       * Anser
   123       * The `Anser` class.
   124       *
   125       * @name Anser
   126       * @function
   127       * @returns {Anser}
   128       */
   129      constructor () {
   130          this.fg = this.bg = this.fg_truecolor = this.bg_truecolor = null;
   131          this.bright = 0;
   132          this.decorations = [];
   133      }
   134  
   135      /**
   136       * setupPalette
   137       * Sets up the palette.
   138       *
   139       * @name setupPalette
   140       * @function
   141       */
   142      setupPalette () {
   143          this.PALETTE_COLORS = [];
   144  
   145          // Index 0..15 : System color
   146          for (let i = 0; i < 2; ++i) {
   147              for (let j = 0; j < 8; ++j) {
   148                  this.PALETTE_COLORS.push(ANSI_COLORS[i][j].color);
   149              }
   150          }
   151  
   152          // Index 16..231 : RGB 6x6x6
   153          // https://gist.github.com/jasonm23/2868981#file-xterm-256color-yaml
   154          let levels = [0, 95, 135, 175, 215, 255];
   155          let format = (r, g, b) => levels[r] + ", " + levels[g] + ", " + levels[b];
   156          let r, g, b;
   157          for (let r = 0; r < 6; ++r) {
   158              for (let g = 0; g < 6; ++g) {
   159                  for (let b = 0; b < 6; ++b) {
   160                      this.PALETTE_COLORS.push(format(r, g, b));
   161                  }
   162              }
   163          }
   164  
   165          // Index 232..255 : Grayscale
   166          let level = 8;
   167          for (let i = 0; i < 24; ++i, level += 10) {
   168              this.PALETTE_COLORS.push(format(level, level, level));
   169          }
   170      }
   171  
   172      /**
   173       * escapeForHtml
   174       * Escapes the input text.
   175       *
   176       * @name escapeForHtml
   177       * @function
   178       * @param {String} txt The input text.
   179       * @returns {String} The escaped HTML output.
   180       */
   181      escapeForHtml (txt) {
   182          return txt.replace(/[&<>\"]/gm, str =>
   183                             str == "&" ? "&amp;" :
   184                                 str == '"' ? "&quot;" :
   185                                 str == "<" ? "&lt;" :
   186                                 str == ">" ? "&gt;" : ""
   187                            );
   188      }
   189  
   190      /**
   191       * linkify
   192       * Adds HTML link elements.
   193       *
   194       * @name linkify
   195       * @function
   196       * @param {String} txt The input text.
   197       * @returns {String} The HTML output containing link elements.
   198       */
   199      linkify (txt) {
   200          return txt.replace(/(https?:\/\/[^\s<>"]+)/gm, str => `<a href="${str}">${str}</a>`);
   201      }
   202  
   203      /**
   204       * ansiToHtml
   205       * Converts ANSI input into HTML output.
   206       *
   207       * @name ansiToHtml
   208       * @function
   209       * @param {String} txt The input text.
   210       * @param {Object} options The options passed ot the `process` method.
   211       * @returns {String} The HTML output.
   212       */
   213      ansiToHtml (txt, options) {
   214          return this.process(txt, options, true);
   215      }
   216  
   217      /**
   218       * ansiToJson
   219       * Converts ANSI input into HTML output.
   220       *
   221       * @name ansiToJson
   222       * @function
   223       * @param {String} txt The input text.
   224       * @param {Object} options The options passed ot the `process` method.
   225       * @returns {String} The JSON output.
   226       */
   227      ansiToJson (txt, options) {
   228          options = options || {};
   229          options.json = true;
   230          options.clearLine = false;
   231          return this.process(txt, options, true);
   232      }
   233  
   234      /**
   235       * ansiToText
   236       * Converts ANSI input into HTML output.
   237       *
   238       * @name ansiToText
   239       * @function
   240       * @param {String} txt The input text.
   241       * @returns {String} The text output.
   242       */
   243      ansiToText (txt) {
   244          return this.process(txt, {}, false);
   245      }
   246  
   247      /**
   248       * process
   249       * Processes the input.
   250       *
   251       * @name process
   252       * @function
   253       * @param {String} txt The input text.
   254       * @param {Object} options An object passed to `processChunk` method, extended with:
   255       *
   256       *  - `json` (Boolean): If `true`, the result will be an object.
   257       *  - `use_classes` (Boolean): If `true`, HTML classes will be appended to the HTML output.
   258       *
   259       * @param {Boolean} markup
   260       */
   261      process (txt, options, markup) {
   262          let self = this;
   263          let raw_text_chunks = txt.split(/\033\[/);
   264          let first_chunk = raw_text_chunks.shift(); // the first chunk is not the result of the split
   265  
   266          if (options === undefined || options === null) {
   267              options = {};
   268          }
   269          options.clearLine = /\r/.test(txt); // check for Carriage Return
   270          let color_chunks = raw_text_chunks.map(chunk => this.processChunk(chunk, options, markup))
   271  
   272          if (options && options.json) {
   273              let first = self.processChunkJson("");
   274              first.content = first_chunk;
   275              first.clearLine = options.clearLine;
   276              color_chunks.unshift(first);
   277              if (options.remove_empty) {
   278                  color_chunks = color_chunks.filter(c => !c.isEmpty());
   279              }
   280              return color_chunks;
   281          } else {
   282              color_chunks.unshift(first_chunk);
   283          }
   284  
   285          return color_chunks.join("");
   286      }
   287  
   288      /**
   289       * processChunkJson
   290       * Processes the current chunk into json output.
   291       *
   292       * @name processChunkJson
   293       * @function
   294       * @param {String} text The input text.
   295       * @param {Object} options An object containing the following fields:
   296       *
   297       *  - `json` (Boolean): If `true`, the result will be an object.
   298       *  - `use_classes` (Boolean): If `true`, HTML classes will be appended to the HTML output.
   299       *
   300       * @param {Boolean} markup If false, the colors will not be parsed.
   301       * @return {Object} The result object:
   302       *
   303       *  - `content` (String): The text.
   304       *  - `fg` (String|null): The foreground color.
   305       *  - `bg` (String|null): The background color.
   306       *  - `fg_truecolor` (String|null): The foreground true color (if 16m color is enabled).
   307       *  - `bg_truecolor` (String|null): The background true color (if 16m color is enabled).
   308       *  - `clearLine` (Boolean): `true` if a carriageReturn \r was fount at end of line.
   309       *  - `was_processed` (Boolean): `true` if the colors were processed, `false` otherwise.
   310       *  - `isEmpty` (Function): A function returning `true` if the content is empty, or `false` otherwise.
   311       *
   312       */
   313      processChunkJson (text, options, markup) {
   314  
   315          // Are we using classes or styles?
   316          options = typeof options == "undefined" ? {} : options;
   317          let use_classes = options.use_classes = typeof options.use_classes != "undefined" && options.use_classes;
   318          let key = options.key = use_classes ? "class" : "color";
   319  
   320          let result = {
   321              content: text
   322            , fg: null
   323            , bg: null
   324            , fg_truecolor: null
   325            , bg_truecolor: null
   326            , isInverted: false
   327            , clearLine: options.clearLine
   328            , decoration: null
   329            , decorations: []
   330            , was_processed: false
   331            , isEmpty: () => !result.content
   332          };
   333  
   334          // Each "chunk" is the text after the CSI (ESC + "[") and before the next CSI/EOF.
   335          //
   336          // This regex matches four groups within a chunk.
   337          //
   338          // The first and third groups match code type.
   339          // We supported only SGR command. It has empty first group and "m" in third.
   340          //
   341          // The second group matches all of the number+semicolon command sequences
   342          // before the "m" (or other trailing) character.
   343          // These are the graphics or SGR commands.
   344          //
   345          // The last group is the text (including newlines) that is colored by
   346          // the other group"s commands.
   347          let matches = text.match(/^([!\x3c-\x3f]*)([\d;]*)([\x20-\x2c]*[\x40-\x7e])([\s\S]*)/m);
   348  
   349          if (!matches) return result;
   350  
   351          let orig_txt = result.content = matches[4];
   352          let nums = matches[2].split(";");
   353  
   354          // We currently support only "SGR" (Select Graphic Rendition)
   355          // Simply ignore if not a SGR command.
   356          if (matches[1] !== "" || matches[3] !== "m") {
   357              return result;
   358          }
   359  
   360          if (!markup) {
   361              return result;
   362          }
   363  
   364          let self = this;
   365  
   366          while (nums.length > 0) {
   367              let num_str = nums.shift();
   368              let num = parseInt(num_str);
   369  
   370              if (isNaN(num) || num === 0) {
   371                  self.fg = self.bg = null;
   372                  self.decorations = [];
   373              } else if (num === 1) {
   374                  self.decorations.push("bold");
   375              } else if (num === 2) {
   376                  self.decorations.push("dim");
   377              // Enable code 2 to get string
   378              } else if (num === 3) {
   379                    self.decorations.push("italic");
   380              } else if (num === 4) {
   381                  self.decorations.push("underline");
   382              } else if (num === 5) {
   383                  self.decorations.push("blink");
   384              } else if (num === 7) {
   385                  self.decorations.push("reverse");
   386              } else if (num === 8) {
   387                  self.decorations.push("hidden");
   388              // Enable code 9 to get strikethrough
   389              } else if (num === 9) {
   390                  self.decorations.push("strikethrough");
   391              /**
   392               * Add several widely used style codes
   393               * @see https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
   394               */
   395              } else if (num === 21) {
   396                  self.removeDecoration("bold");
   397              } else if (num === 22) {
   398                  self.removeDecoration("bold");
   399                  self.removeDecoration("dim");
   400              } else if (num === 23) {
   401                  self.removeDecoration("italic");
   402              } else if (num === 24) {
   403                  self.removeDecoration("underline");
   404              } else if (num === 25) {
   405                  self.removeDecoration("blink");
   406              } else if (num === 27) {
   407                  self.removeDecoration("reverse");
   408              } else if (num === 28) {
   409                  self.removeDecoration("hidden");
   410              } else if (num === 29) {
   411                  self.removeDecoration("strikethrough");
   412              } else if (num === 39) {
   413                  self.fg = null;
   414              } else if (num === 49) {
   415                  self.bg = null;
   416              // Foreground color
   417              } else if ((num >= 30) && (num < 38)) {
   418                  self.fg = ANSI_COLORS[0][(num % 10)][key];
   419              // Foreground bright color
   420              } else if ((num >= 90) && (num < 98)) {
   421                  self.fg = ANSI_COLORS[1][(num % 10)][key];
   422              // Background color
   423              } else if ((num >= 40) && (num < 48)) {
   424                  self.bg = ANSI_COLORS[0][(num % 10)][key];
   425              // Background bright color
   426              } else if ((num >= 100) && (num < 108)) {
   427                  self.bg = ANSI_COLORS[1][(num % 10)][key];
   428              } else if (num === 38 || num === 48) { // extend color (38=fg, 48=bg)
   429                  let is_foreground = (num === 38);
   430                  if (nums.length >= 1) {
   431                      let mode = nums.shift();
   432                      if (mode === "5" && nums.length >= 1) { // palette color
   433                          let palette_index = parseInt(nums.shift());
   434                          if (palette_index >= 0 && palette_index <= 255) {
   435                              if (!use_classes) {
   436                                  if (!this.PALETTE_COLORS) {
   437                                      self.setupPalette();
   438                                  }
   439                                  if (is_foreground) {
   440                                      self.fg = this.PALETTE_COLORS[palette_index];
   441                                  } else {
   442                                      self.bg = this.PALETTE_COLORS[palette_index];
   443                                  }
   444                              } else {
   445                                  let klass = (palette_index >= 16)
   446                                      ? ("ansi-palette-" + palette_index)
   447                                      : ANSI_COLORS[palette_index > 7 ? 1 : 0][palette_index % 8]["class"];
   448                                      if (is_foreground) {
   449                                          self.fg = klass;
   450                                      } else {
   451                                          self.bg = klass;
   452                                      }
   453                              }
   454                          }
   455                      } else if(mode === "2" && nums.length >= 3) { // true color
   456                          let r = parseInt(nums.shift());
   457                          let g = parseInt(nums.shift());
   458                          let b = parseInt(nums.shift());
   459                          if ((r >= 0 && r <= 255) && (g >= 0 && g <= 255) && (b >= 0 && b <= 255)) {
   460                              let color = r + ", " + g + ", " + b;
   461                              if (!use_classes) {
   462                                  if (is_foreground) {
   463                                      self.fg = color;
   464                                  } else {
   465                                      self.bg = color;
   466                                  }
   467                              } else {
   468                                  if (is_foreground) {
   469                                      self.fg = "ansi-truecolor";
   470                                      self.fg_truecolor = color;
   471                                  } else {
   472                                      self.bg = "ansi-truecolor";
   473                                      self.bg_truecolor = color;
   474                                  }
   475                              }
   476                          }
   477                      }
   478                  }
   479              }
   480          }
   481  
   482          if ((self.fg === null) && (self.bg === null) && (self.decorations.length === 0)) {
   483              return result;
   484          } else {
   485              let styles = [];
   486              let classes = [];
   487              let data = {};
   488  
   489              result.fg = self.fg;
   490              result.bg = self.bg;
   491              result.fg_truecolor = self.fg_truecolor;
   492              result.bg_truecolor = self.bg_truecolor;
   493              result.decorations = self.decorations;
   494              result.decoration = self.decorations.slice(-1).pop() || null;
   495              result.was_processed = true;
   496  
   497              return result;
   498          }
   499      }
   500  
   501      /**
   502       * processChunk
   503       * Processes the current chunk of text.
   504       *
   505       * @name processChunk
   506       * @function
   507       * @param {String} text The input text.
   508       * @param {Object} options An object containing the following fields:
   509       *
   510       *  - `json` (Boolean): If `true`, the result will be an object.
   511       *  - `use_classes` (Boolean): If `true`, HTML classes will be appended to the HTML output.
   512       *
   513       * @param {Boolean} markup If false, the colors will not be parsed.
   514       * @return {Object|String} The result (object if `json` is wanted back or string otherwise).
   515       */
   516      processChunk (text, options, markup) {
   517          options = options || {};
   518          let jsonChunk = this.processChunkJson(text, options, markup);
   519          let use_classes = options.use_classes;
   520  
   521          // "reverse" decoration reverses foreground and background colors
   522          jsonChunk.decorations = jsonChunk.decorations
   523              .filter((decoration) => {
   524                  if (decoration === "reverse") {
   525                      // when reversing, missing colors are defaulted to black (bg) and white (fg)
   526                      if (!jsonChunk.fg) {
   527                        jsonChunk.fg = ANSI_COLORS[0][7][use_classes ? "class" : "color"];
   528                      }
   529                      if (!jsonChunk.bg) {
   530                        jsonChunk.bg = ANSI_COLORS[0][0][use_classes ? "class" : "color"];
   531                      }
   532                      let tmpFg = jsonChunk.fg;
   533                      jsonChunk.fg = jsonChunk.bg;
   534                      jsonChunk.bg = tmpFg;
   535                      let tmpFgTrue = jsonChunk.fg_truecolor;
   536                      jsonChunk.fg_truecolor = jsonChunk.bg_truecolor;
   537                      jsonChunk.bg_truecolor = tmpFgTrue;
   538                      jsonChunk.isInverted = true;
   539                      return false;
   540                  }
   541                  return true;
   542              });
   543  
   544          if (options.json) { return jsonChunk; }
   545          if (jsonChunk.isEmpty()) { return ""; }
   546          if (!jsonChunk.was_processed) { return jsonChunk.content; }
   547  
   548          let colors = [];
   549          let decorations = [];
   550          let textDecorations = [];
   551          let data = {};
   552  
   553          let render_data = data => {
   554              let fragments = [];
   555              let key;
   556              for (key in data) {
   557                  if (data.hasOwnProperty(key)) {
   558                      fragments.push("data-" + key + "=\"" + this.escapeForHtml(data[key]) + "\"");
   559                  }
   560              }
   561              return fragments.length > 0 ? " " + fragments.join(" ") : "";
   562          };
   563  
   564          if (jsonChunk.isInverted) {
   565            data["ansi-is-inverted"] = "true";
   566          }
   567  
   568          if (jsonChunk.fg) {
   569              if (use_classes) {
   570                  colors.push(jsonChunk.fg + "-fg");
   571                  if (jsonChunk.fg_truecolor !== null) {
   572                      data["ansi-truecolor-fg"] = jsonChunk.fg_truecolor;
   573                      jsonChunk.fg_truecolor = null;
   574                  }
   575              } else if (jsonChunk.fg[0] == '#') {
   576                  colors.push("color:" + jsonChunk.fg + ";");
   577              } else {
   578                  colors.push("color:rgb(" + jsonChunk.fg + ")");
   579              }
   580          }
   581  
   582          if (jsonChunk.bg) {
   583              if (use_classes) {
   584                  colors.push(jsonChunk.bg + "-bg");
   585                  if (jsonChunk.bg_truecolor !== null) {
   586                      data["ansi-truecolor-bg"] = jsonChunk.bg_truecolor;
   587                      jsonChunk.bg_truecolor = null;
   588                  }
   589              } else if (jsonChunk.bg[0] == '#') {
   590                  colors.push("background-color:" + jsonChunk.bg + ";");
   591              } else {
   592                colors.push("background-color:rgb(" + jsonChunk.bg + ")");
   593              }
   594          }
   595  
   596          jsonChunk.decorations.forEach((decoration) => {
   597              // use classes
   598              if (use_classes) {
   599                  decorations.push("ansi-" + decoration);
   600                  return;
   601              }
   602              // use styles
   603              if (decoration === "bold") {
   604                  decorations.push("font-weight:bold");
   605              } else if (decoration === "dim") {
   606                  decorations.push("opacity:0.5");
   607              } else if (decoration === "italic") {
   608                  decorations.push("font-style:italic");
   609              } else if (decoration === "hidden") {
   610                  decorations.push("visibility:hidden");
   611              } else if (decoration === "strikethrough") {
   612                  textDecorations.push("line-through");
   613              } else {
   614                  // underline and blink are treated here
   615                  textDecorations.push(decoration);
   616              }
   617          });
   618  
   619          if (textDecorations.length) {
   620              decorations.push("text-decoration:" + textDecorations.join(" "));
   621          }
   622  
   623          if (use_classes) {
   624              return "<span class=\"" + colors.concat(decorations).join(" ") + "\"" + render_data(data) + ">" + jsonChunk.content + "</span>";
   625          } else {
   626              return "<span style=\"" + colors.concat(decorations).join(";") + "\"" + render_data(data) + ">" + jsonChunk.content + "</span>";
   627          }
   628      }
   629  
   630      removeDecoration(decoration) {
   631          const index = this.decorations.indexOf(decoration);
   632  
   633          if (index >= 0) {
   634              this.decorations.splice(index, 1);
   635          }
   636      }
   637  }
   638  
   639  export default Anser