1 /* 2 This file is part of ath (ansi to html), ported from aha 3 4 Copyright (C) 2022 Malte Jürgens <maltejur@dismail.de> 5 Copyright (C) 2012-2021 Alexander Matthes (Ziz) , ziz_at_mailbox.org 6 7 ath is free software: you can redistribute it and/or modify it under the 8 terms of the GNU Lesser General Public License as published by the Free 9 Software Foundation, either version 3 of the License, or (at your option) any 10 later version. 11 12 ath is distributed in the hope that it will be useful, but WITHOUT ANY 13 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 14 PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 15 details. 16 17 You should have received a copy of the GNU Lesser General Public License along 18 with ath. If not, see <https://www.gnu.org/licenses/>. 19 */ 20 21 module ath; 22 23 import std.format : format; 24 import std.stdio : File, stdout, stderr, writef, writefln; 25 import std.process : Pipe, pipe; 26 import std.array : join; 27 import std.ascii : newline; 28 import std.algorithm : max, min; 29 import std.algorithm.mutation : swap; 30 import core.stdc.stdio : fgetc, EOF, FILE; 31 32 class AthException : Exception 33 { 34 this( 35 string msg, 36 string file = __FILE__, 37 size_t line = __LINE__, 38 Throwable nextInChain = null 39 ) pure nothrow @nogc @safe 40 { 41 super(msg, file, line, nextInChain); 42 } 43 } 44 45 struct AthOptions 46 { 47 bool noBuffer = false; 48 bool dark = false; 49 bool document = false; 50 bool noPre = false; 51 } 52 53 const string[] palleteDark = [ 54 "dimgray", "red", "lime", "yellow", "#3333FF", "fuchsia", "aqua", "white", 55 "black", "white" 56 ]; 57 const string[] palleteLight = [ 58 "dimgray", "red", "green", "olive", "blue", "purple", "teal", "gray", "white", 59 "black" 60 ]; 61 62 enum ColorMode 63 { 64 MODE_3BIT, 65 MODE_8BIT, 66 MODE_24BIT 67 } 68 69 private struct State 70 { 71 int fc; 72 int bc; 73 bool bold; 74 bool faint; 75 bool italic; 76 bool underline; 77 bool blink; 78 bool crossedout; 79 bool inverted; 80 bool hidden; 81 bool overlined; 82 ColorMode fc_colormode; 83 ColorMode bc_colormode; 84 bool fc_highlighted; 85 bool bc_highlighted; 86 } 87 88 private struct Cell 89 { 90 char value; 91 State state; 92 bool empty = true; 93 94 this(char value, State state) 95 { 96 this.value = value; 97 this.state = state; 98 this.empty = false; 99 } 100 } 101 102 private const State defaultState = { 103 fc: -1, 104 bc: -1, 105 bold: false, 106 faint: false, 107 italic: false, 108 underline: false, 109 blink: false, 110 crossedout: false, 111 inverted: false, 112 hidden: false, 113 overlined: false, 114 fc_colormode: ColorMode.MODE_3BIT, 115 bc_colormode: ColorMode.MODE_3BIT, 116 fc_highlighted: false, 117 bc_highlighted: false 118 }; 119 120 private int getNextChar(FILE* fp) 121 { 122 123 int c; 124 if ((c = fgetc(fp)) != EOF) 125 { 126 // stderr.write(cast(char)(c)); 127 return c; 128 } 129 130 throw new AthException("Unexpected EOF"); 131 } 132 133 private string make_rgb(int color_id) 134 { 135 if (color_id < 16 || color_id > 255) 136 return "#000000"; 137 if (color_id >= 232) 138 { 139 int index = color_id - 232; 140 int grey = index * 256 / 24; 141 return format("#%02x%02x%02x", grey, grey, grey); 142 } 143 144 int index_R = (color_id - 16) / 36; 145 int rgb_R; 146 if (index_R > 0) 147 rgb_R = 55 + index_R * 40; 148 else 149 rgb_R = 0; 150 151 int index_G = ((color_id - 16) % 36) / 6; 152 int rgb_G; 153 if (index_G > 0) 154 rgb_G = 55 + index_G * 40; 155 else 156 rgb_G = 0; 157 158 int index_B = ((color_id - 16) % 6); 159 int rgb_B; 160 if (index_B > 0) 161 rgb_B = 55 + index_B * 40; 162 else 163 rgb_B = 0; 164 165 return format("#%02x%02x%02x", rgb_R, rgb_G, rgb_B); 166 } 167 168 private void swapColors(State* state) 169 { 170 state.inverted = !state.inverted; 171 172 if (state.bc_colormode == ColorMode.MODE_3BIT && state.bc == -1) 173 state.bc = -2; 174 if (state.fc_colormode == ColorMode.MODE_3BIT && state.fc == -1) 175 state.fc = -2; 176 177 swap(state.fc, state.bc); 178 swap(state.fc_colormode, state.bc_colormode); 179 swap(state.fc_highlighted, state.bc_highlighted); 180 } 181 182 void ansi_to_html(File input, File output, AthOptions options = AthOptions()) 183 { 184 Cell[][] buf; 185 int c; 186 int line = 0; 187 int saved_line = 0; 188 int col = 0; 189 int saved_col = 0; 190 State state = defaultState; 191 State oldState = defaultState; 192 193 void ensureBuffer() 194 { 195 buf.length = max(buf.length, line + 1); 196 buf[line].length = max(buf[line].length, col + 1); 197 } 198 199 string escapeHtml(char c) 200 { 201 string s = [c]; 202 203 if (s == "&") 204 s = "&"; 205 if (s == "<") 206 s = "<"; 207 if (s == ">") 208 s = ">"; 209 if (s == "\"") 210 s = """; 211 212 return s; 213 } 214 215 void write(char c) 216 { 217 if (options.noBuffer) 218 output.write(escapeHtml(c)); 219 else 220 { 221 ensureBuffer(); 222 buf[line][col] = Cell(c, state); 223 } 224 } 225 226 void handleNewState(State oldState, State newState) 227 { 228 //Checking the differences 229 if (newState != oldState) 230 { 231 232 // If old state was different than the default one, close the current <span> 233 if (oldState != defaultState) 234 output.write("</span>"); 235 236 // Open new <span> if current state differs from the default one 237 if (newState != defaultState) 238 { 239 output.write("<span style=\""); 240 241 if (newState.bold) 242 output.write("font-weight:bold;"); 243 else if (newState.faint) 244 output.write("font-weight:lighter;"); 245 246 if (newState.italic) 247 output.write("font-style:italic;"); 248 249 if (newState.underline || newState.blink || newState.crossedout || newState.overlined) 250 { 251 output.write("text-decoration:"); 252 253 if (newState.underline) 254 output.write(" underline"); 255 if (newState.blink) 256 output.write(" blink"); 257 if (newState.crossedout) 258 output.write(" line-through"); 259 if (newState.overlined) 260 output.write(" overline"); 261 262 output.write(";"); 263 } 264 265 if (newState.hidden) 266 output.write("opacity:0;"); 267 268 if (newState.fc_highlighted || newState.bc_highlighted) 269 output.write("filter: contrast(70%) brightness(190%);"); 270 271 const string default_fc_color = options.dark ? "lightgray" : "black"; 272 string fc_color = default_fc_color; 273 final switch (newState.fc_colormode) 274 { 275 case ColorMode.MODE_3BIT: 276 if (newState.fc >= 0 && newState.fc <= 9) 277 fc_color = options.dark ? palleteDark[newState.fc] : palleteLight[newState.fc]; 278 break; 279 280 case ColorMode.MODE_8BIT: 281 if (newState.fc >= 0 && newState.fc <= 7) 282 fc_color = options.dark ? palleteDark[newState.fc] : palleteLight[newState.fc]; 283 else 284 fc_color = make_rgb(newState.fc); 285 break; 286 287 case ColorMode.MODE_24BIT: 288 fc_color = format("#%06x", newState.fc); 289 break; 290 } 291 292 const string default_bc_color = options.dark ? "black" : "lightgray"; 293 string bc_color = default_bc_color; 294 final switch (newState.bc_colormode) 295 { 296 case ColorMode.MODE_3BIT: 297 if (newState.bc >= 0 && newState.bc <= 9) 298 bc_color = options.dark ? palleteDark[newState.bc] : palleteLight[newState.bc]; 299 break; 300 301 case ColorMode.MODE_8BIT: 302 if (newState.bc >= 0 && newState.bc <= 7) 303 bc_color = options.dark ? palleteDark[newState.bc] : palleteLight[newState.bc]; 304 else 305 bc_color = make_rgb(newState.bc); 306 break; 307 308 case ColorMode.MODE_24BIT: 309 bc_color = format("#%06x", newState.bc); 310 break; 311 } 312 313 if (newState.inverted) 314 swap(fc_color, bc_color); 315 316 if (fc_color != default_fc_color) 317 output.write(format("color:%s;", fc_color)); 318 if (bc_color != default_bc_color) 319 output.write(format("background-color:%s;", bc_color)); 320 321 output.write("\">"); 322 } 323 } 324 } 325 326 /// Make sure new state is still picked up when jumping to new cursor position 327 void saveStateEmpty() 328 { 329 ensureBuffer(); // buf[line] is accessed, gotta make sure it exists 330 if (col + 1 >= buf[line].length || buf[line][col + 1].empty) 331 { 332 col += 1; 333 ensureBuffer(); 334 buf[line][col] = Cell(' ', state); 335 col -= 1; 336 } 337 } 338 339 void newline() 340 { 341 if (options.noBuffer) 342 output.write("\n"); 343 else 344 saveStateEmpty(); 345 line += 1; 346 col = 0; 347 } 348 349 void carriageReturn() 350 { 351 if (options.noBuffer) 352 return; 353 saveStateEmpty(); 354 col = 0; 355 } 356 357 void moveCursor(int deltaX, int deltaY) 358 { 359 if (options.noBuffer) 360 return; 361 362 saveStateEmpty(); 363 364 // Ensure bounds 365 line = max(line + deltaY, 0); 366 col = max(col + deltaX, 0); 367 } 368 369 FILE* fp = input.getFP(); 370 371 if (options.document) 372 { 373 output.writeln("<!DOCTYPE html> 374 <head> 375 <meta charset=\"UTF-8\"> 376 <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"> 377 <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"> 378 <title>ath</title>"); 379 if (options.dark) 380 output.writeln("<style>body{background-color:black;color:lightgray;}</style>"); 381 output.writeln("</head> 382 <body> 383 <pre>"); 384 } 385 else if (!options.noPre) 386 { 387 if (options.dark) 388 output.write("<pre style=\"background-color:black;color:lightgray;\">"); 389 else 390 output.write("<pre>"); 391 } 392 393 while ((c = fgetc(fp)) != EOF) 394 { 395 // ESC 396 if (c == '\033') 397 { 398 // Searching the end (a letter) and safe the insert: 399 c = getNextChar(fp); 400 if (c == '[') // CSI code, see https://en.wikipedia.org/wiki/ANSI_escape_code#Colors 401 { 402 int[] elems; 403 int elem = 0; 404 int value = 0; 405 406 while (true) 407 { 408 c = getNextChar(fp); 409 410 if (((c >= 'A') && (c <= 'Z')) || ((c >= 'a') && (c <= 'z'))) 411 { 412 elems ~= value; 413 break; 414 } 415 416 if (c == ';' || c == ':' || c == 0) 417 { 418 elems ~= value; 419 value = 0; 420 continue; 421 } 422 423 value = (value * 10) + (c - '0'); 424 } 425 426 switch (c) 427 { 428 case 'A': // Cursor Up 429 int deltaY = elems[0] == 0 ? -1 : -elems[0]; 430 moveCursor(0, deltaY); 431 break; 432 433 case 'B': // Cursor Down 434 int deltaY = elems[0] == 0 ? 1 : elems[0]; 435 moveCursor(0, deltaY); 436 break; 437 438 case 'C': // Cursor Forward 439 int deltaX = elems[0] == 0 ? 1 : elems[0]; 440 moveCursor(deltaX, 0); 441 break; 442 443 case 'D': // Cursor Back 444 int deltaX = elems[0] == 0 ? -1 : -elems[0]; 445 moveCursor(deltaX, 0); 446 break; 447 448 case 'E': // Cursor Next Line 449 carriageReturn(); 450 int deltaY = elems[0] == 0 ? 1 : elems[0]; 451 moveCursor(0, deltaY); 452 break; 453 454 case 'F': // Cursor Previous Line 455 carriageReturn(); 456 int deltaY = elems[0] == 0 ? -1 : -elems[0]; 457 moveCursor(0, deltaY); 458 break; 459 460 case 'G': // Cursor Horizontal Absolute 461 int newCol = elems[0] == 0 ? 1 : elems[0]; 462 moveCursor(newCol - 1 - col, 0); 463 break; 464 465 case 'H': // Cursor Position 466 case 'f': // Horizontal Vertical Position 467 int newLine = elems[0] == 0 ? 1 : elems[0]; 468 int newCol = (elems.length == 1 || elems[1] == 0) ? 1 : elems[1]; 469 moveCursor(newCol - 1 - col, newLine - 1 - line); 470 break; 471 472 case 'J': // Erase in Display 473 switch (elems[0]) 474 { 475 case 0: // Clear from cursor to end of screen 476 buf.length = line + 1; 477 for (int i = col; i < buf[line].length; i += 1) 478 buf[line][col].empty = true; 479 break; 480 481 case 1: // Clear from cursor to beginning of the screen 482 ulong height = buf.length; 483 Cell[][] tmp = buf[line .. buf.length]; 484 buf.length = 0; 485 buf.length = height; 486 buf[line .. buf.length] = tmp; 487 for (int i = col; i >= 0; i -= 1) 488 buf[line][col].empty = true; 489 break; 490 491 case 2: 492 case 3: 493 buf.length = 0; 494 buf.length = line + 1; 495 break; 496 497 default: 498 break; 499 } 500 break; 501 502 case 'K': // Erase in Line 503 switch (elems[0]) 504 { 505 506 case 0: // Clear from cursor to the end of the line 507 for (int i = col; i < buf[line].length; i += 1) 508 buf[line][col].empty = true; 509 break; 510 511 case 1: // Clear from cursor to beginning of the line 512 for (int i = col; i >= 0; i -= 1) 513 buf[line][col].empty = true; 514 break; 515 516 case 2: 517 for (int i = 0; i < buf[line].length; i += 1) 518 buf[line][col].empty = true; 519 break; 520 521 default: 522 break; 523 } 524 break; 525 526 case 'm': 527 while (elem < elems.length) 528 { 529 switch (elems[elem]) 530 { 531 case 0: // 0 - Reset all 532 state = defaultState; 533 break; 534 535 case 1: // 1 - Enable Bold 536 state.bold = true; 537 break; 538 539 case 2: // 2 - Enable Faint 540 state.faint = true; 541 break; 542 543 case 3: // 3 - Enable Italic 544 state.italic = true; 545 break; 546 547 case 4: // 4 - Enable underline 548 state.underline = true; 549 break; 550 551 case 5: // 5 - Slow Blink 552 state.blink = true; 553 break; 554 555 case 7: // 7 - Inverse video 556 state.inverted = true; 557 break; 558 559 case 8: // 8 - Conceal or hide 560 state.hidden = true; 561 break; 562 563 case 9: // 9 - Enable hide 564 state.crossedout = true; 565 break; 566 567 case 21: // 21 - Reset bold 568 case 22: // 22 - Not bold, not "high intensity" color 569 state.bold = false; 570 state.faint = false; 571 break; 572 573 case 23: // 23 - Reset italic 574 state.italic = false; 575 break; 576 577 case 24: // 23 - Reset underline 578 state.underline = false; 579 break; 580 581 case 25: // 25 - Reset blink 582 state.blink = false; 583 break; 584 585 case 27: // 27 - Reset Inverted 586 state.inverted = false; 587 break; 588 589 case 28: // 28 - Reveal 590 state.hidden = false; 591 break; 592 593 case 29: // 29 - Reset crossed-out 594 state.crossedout = false; 595 break; 596 597 case 30: 598 case 31: 599 case 32: 600 case 33: 601 case 34: 602 case 35: 603 case 36: 604 case 37: // 30-37 - Set foreground color (3 bit) 605 state.fc_colormode = ColorMode.MODE_3BIT; 606 state.fc = elems[elem] - 30; 607 break; 608 609 case 38: 610 if (elems[elem + 1] == 5) // 38 - Set foreground color (8 bit) 611 { 612 state.fc_colormode = ColorMode.MODE_8BIT; 613 if (elems[elem + 2] >= 8 && elems[elem + 2] <= 15) 614 { 615 state.fc = elems[elem + 2] - 8; 616 state.fc_highlighted = true; 617 } 618 else 619 { 620 state.fc = elems[elem + 2]; 621 state.fc_highlighted = false; 622 } 623 elem += 2; 624 } 625 else if (elems[elem + 1] == 2) // 38 - Set foreground color (24 bit) 626 { 627 int r = elems[elem + 2]; 628 int g = elems[elem + 3]; 629 int b = elems[elem + 4]; 630 631 state.fc_colormode = ColorMode.MODE_24BIT; 632 state.fc = 633 (r & 255) * 65_536 + 634 (g & 255) * 256 + 635 (b & 255); 636 state.fc_highlighted = false; 637 638 elem += 4; 639 } 640 break; 641 642 case 39: // 39 - Default foreground color 643 state.fc_colormode = ColorMode.MODE_3BIT; 644 state.fc = -1; 645 state.fc_highlighted = false; 646 break; 647 648 case 40: 649 case 41: 650 case 42: 651 case 43: 652 case 44: 653 case 45: 654 case 46: 655 case 47: // 40-47 - Set background color (3 bit) 656 state.bc_colormode = ColorMode.MODE_3BIT; 657 state.bc = elems[elem] - 40; 658 break; 659 660 case 48: 661 if (elems[elem + 1] == 5) // 48 - Set background color (8 bit) 662 { 663 state.bc_colormode = ColorMode.MODE_8BIT; 664 if (elems[elem + 2] >= 8 && elems[elem + 2] <= 15) 665 { 666 state.bc = elems[elem + 2] - 8; 667 state.bc_highlighted = true; 668 } 669 else 670 { 671 state.bc = elems[elem + 2]; 672 state.bc_highlighted = false; 673 } 674 elem += 2; 675 } 676 else if (elems[elem + 1] == 2) // 48 - Set background color (24 bit) 677 { 678 int r = elems[elem + 2]; 679 int g = elems[elem + 3]; 680 int b = elems[elem + 4]; 681 682 state.bc_colormode = ColorMode.MODE_24BIT; 683 state.bc = 684 (r & 255) * 65_536 + 685 (g & 255) * 256 + 686 (b & 255); 687 state.bc_highlighted = false; 688 689 elem += 4; 690 } 691 break; 692 693 case 49: // 49 - Default background color 694 state.bc_colormode = ColorMode.MODE_3BIT; 695 state.bc = -1; 696 state.bc_highlighted = false; 697 break; 698 699 case 53: // 53 - Overlined 700 state.overlined = true; 701 break; 702 703 case 55: // 55 - Not overlined 704 state.overlined = false; 705 break; 706 707 default: 708 break; 709 } 710 elem += 1; 711 } 712 break; 713 714 case 's': 715 saved_line = line; 716 saved_col = col; 717 break; 718 719 case 'u': 720 line = saved_line; 721 col = saved_col; 722 buf.length = max(buf.length, line + 1); 723 break; 724 725 default: 726 break; 727 } 728 729 if (options.noBuffer) 730 handleNewState(oldState, state); 731 } 732 } 733 else if (c == '\n') 734 newline(); 735 else if (c == '\r') 736 carriageReturn(); 737 else 738 { 739 write(cast(char)(c)); 740 col += 1; 741 } 742 743 oldState = state; 744 } 745 746 if (!options.noBuffer) 747 foreach (size_t i, Cell[] lineContent; buf) 748 { 749 foreach (size_t y, Cell cell; lineContent) 750 if (!cell.empty) 751 { 752 handleNewState(oldState, cell.state); 753 output.write(escapeHtml(cell.value)); 754 oldState = cell.state; 755 } 756 else 757 { 758 handleNewState(oldState, oldState); 759 output.write(" "); 760 } 761 if (i + 1 != buf.length) 762 { 763 handleNewState(state, defaultState); 764 output.write("\n"); 765 } 766 } 767 768 handleNewState(state, defaultState); 769 770 if (options.document) 771 output.write("</pre> 772 </body> 773 </html>"); 774 else if (!options.noPre) 775 output.write("</pre>"); 776 777 output.close(); 778 } 779 780 File ansi_to_html(File input, AthOptions options = AthOptions()) 781 { 782 Pipe output = pipe(); 783 ansi_to_html(input, output.writeEnd, options); 784 return output.readEnd; 785 } 786 787 string ansi_to_html(string ansi, AthOptions options = AthOptions()) 788 { 789 Pipe input = pipe(); 790 input.writeEnd.write(ansi); 791 input.writeEnd.close(); 792 File result = ansi_to_html(input.readEnd, options); 793 return cast(string)(result.byLine.join(newline)); 794 }