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 = "&amp;";
205     if (s == "<")
206       s = "&lt;";
207     if (s == ">")
208       s = "&gt;";
209     if (s == "\"")
210       s = "&quot;";
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 }