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 == "&" ? "&" : 184 str == '"' ? """ : 185 str == "<" ? "<" : 186 str == ">" ? ">" : "" 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