1 /**
2  * @TODO I'm hardcoding this to ANSI for now, as it's the most common.
3  *       Ideally we should have configurable control characters for various
4  *       types of terminals that could be emulated via a standard termcap file.
5  * @NOTE ANSI escape sequence reference:
6  *       https://en.wikipedia.org/wiki/ANSI_escape_code#Escape_sequences
7  *       Note that escape sequences for other terminal types are a thing, and
8  *       currently aren't implemented yet as mentioned in the TODO above, eg:
9  *       https://en.wikipedia.org/wiki/VT52#Escape_sequences
10  */
11 #include "renderhelpers.hpp"
12 #include "kristall.hpp"
13 
14 #include <QByteArray>
15 #include <QString>
16 #include <QTextCursor>
17 #include <QTextFrame>
18 #include <QTextFrameFormat>
19 
20 #include <string>
21 #include <iostream>
22 
23 static constexpr const char escapeString = '\033';
24 bool inverted{false};
25 
setColor(QTextCharFormat & format,unsigned char n,bool bg=false)26 static void setColor(QTextCharFormat& format, unsigned char n, bool bg=false)
27 {
28     QColor color;
29 
30     if (n < 16)
31     {
32         // The normal pre-defined typical 16 colors.
33         color = QColor(kristall::globals().document_style.ansi_colors[n]);
34     }
35     else if (n < 232)
36     {
37         // indexed 8-bit rgb color pallete.
38         unsigned int index_R = ((n - 16) / 36);
39         unsigned char r = index_R > 0 ? 55 + index_R * 40 : 0;
40         unsigned int index_G = (((n - 16) % 36) / 6);
41         unsigned char g = index_G > 0 ? 55 + index_G * 40 : 0;
42         unsigned int index_B = ((n - 16) % 6);
43         unsigned char b = index_B > 0 ? 55 + index_B * 40 : 0;
44 
45         color = QColor(r, g, b);
46     }
47     else
48     {
49         // grayscale pallete.
50         unsigned char g = (n - 232) * 10 + 8;
51         color = QColor(g, g, g);
52     }
53 
54     if (bg)
55         format.setBackground(color);
56     else
57         format.setForeground(color);
58 }
59 
parseNumber(const QString & input,QString::const_iterator & it)60 static QString parseNumber(const QString& input, QString::const_iterator& it)
61 {
62     QString result;
63     while (it != input.cend())
64     {
65         const auto currentCharacter = *it;
66         if (!currentCharacter.isNumber()) break;
67         result += currentCharacter;
68         ++it;
69     }
70     return result;
71 }
72 
parseSGR(std::vector<unsigned char> & args,const QString & input,QString::const_iterator & it,QTextCharFormat & format,const QTextCharFormat & defaultFormat,QTextCursor & cursor)73 static void parseSGR(
74     std::vector<unsigned char>& args,
75     const QString& input,
76     QString::const_iterator& it,
77     QTextCharFormat& format,
78     const QTextCharFormat& defaultFormat,
79     QTextCursor& cursor)
80 {
81     if (args.empty()) return;
82     for (auto it = args.cbegin(); it != args.cend(); ++it)
83     {
84         /// @TODO A whole bunch of unimplemented SGR codes are unimplemented
85         ///       yet (eg: blink or font switching)
86         enum {
87             Reset = 0, Bold, Light, Italic, Underline,
88             Reverse = 7,
89             StrikeOut = 9,
90 
91             // some implementations interpret 21 as Bold off
92             DoubleUnderline = 21, NormalWeight, ItalicOff, UnderlineOff,
93             ReverseOff = 27,
94             StrikeOutOff = 29,
95 
96             // 30-37 and 40-47 color codes are handled in the default case
97             SetForeground = 38,
98             DefaultForeground = 39,
99             SetBackground = 48,
100             DefaultBackground = 49,
101         };
102 
103         const auto arg = *it;
104         switch(arg)
105         {
106             case Reset:
107                 format = defaultFormat;
108                 break;
109             case Bold:
110                 format.setFontWeight(QFont::Bold);
111                 break;
112             case Light:
113                 format.setFontWeight(QFont::Light);
114                 break;
115             case Italic:
116                 format.setFontItalic(true);
117                 break;
118             case Underline:
119                 /// @TODO Underline style should be configurable?
120                 format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
121                 break;
122             case Reverse:
123                 if (!inverted)
124                 {
125                     const auto fg = format.foreground();
126                     const auto bg = format.background();
127                     format.setForeground(bg);
128                     format.setBackground(fg);
129                     inverted = true;
130                 }
131                 break;
132             case StrikeOut:
133                 format.setFontStrikeOut(true);
134                 break;
135             case DoubleUnderline:
136                 format.setUnderlineStyle(QTextCharFormat::WaveUnderline);
137                 break;
138             case NormalWeight:
139                 format.setFontWeight(QFont::Normal);
140                 break;
141             case ItalicOff:
142                 format.setFontItalic(false);
143                 break;
144             case UnderlineOff:
145                 format.setFontUnderline(QTextCharFormat::NoUnderline);
146                 break;
147             case ReverseOff:
148                 if (inverted)
149                 {
150                     const auto fg = format.foreground();
151                     const auto bg = format.background();
152                     format.setForeground(bg);
153                     format.setBackground(fg);
154                     inverted = false;
155                 }
156                 break;
157             case StrikeOutOff:
158                 format.setFontStrikeOut(false);
159                 break;
160             case SetForeground:
161                 if (args.size() > 2)
162                 {
163                     const auto colMode = *++it;
164                     if (colMode == 5)
165                     {
166                         const auto colNum = *++it;
167                         setColor(format, colNum);
168                     }
169                     else if (colMode == 2)
170                     {
171                         ++it;
172                         if (args.size() >= 4)
173                         {
174                             const auto red = *it;
175                             const auto green = *++it;
176                             const auto blue = *++it;
177                             format.setForeground(QColor(red, green, blue));
178                         }
179                     }
180                 }
181                 break;
182             case DefaultForeground:
183                 format.setForeground(defaultFormat.foreground());
184                 break;
185             case SetBackground:
186                 if (args.size() > 2)
187                 {
188                     const auto colMode = *++it;
189                     if (colMode == 5)
190                     {
191                         const auto colNum = *++it;
192                         setColor(format, colNum, true);
193                     }
194                     else if (colMode == 2)
195                     {
196                         ++it;
197                         if (args.size() >= 4)
198                         {
199                             const auto red = *it;
200                             const auto green = *++it;
201                             const auto blue = *++it;
202                             format.setBackground(QColor(red, green, blue));
203                         }
204                     }
205                 }
206                 break;
207             case DefaultBackground:
208                 format.setBackground(defaultFormat.background());
209                 break;
210             default:
211                 // foreground, background and their bright equivalents
212                 if (arg >= 30 && arg < 38)
213                     setColor(format, arg - 30);
214                 else if (arg >= 40 && arg < 48)
215                     setColor(format, arg - 40, true);
216                 else if (arg >= 90 && arg < 98)
217                     setColor(format, arg - 90 + 8);
218                 else if (arg >= 100 && arg < 108)
219                     setColor(format, arg - 100 + 8, true);
220 
221                 break;
222         }
223     }
224 }
225 
parseNumericArguments(const QString & input,QString::const_iterator & it)226 static std::vector<unsigned char> parseNumericArguments(const QString& input, QString::const_iterator& it)
227 {
228     std::vector<unsigned char> result;
229     while (it != input.end())
230     {
231         const auto currentCharacter = *it;
232         const auto numStr = parseNumber(input, it);
233         if (numStr.isEmpty())
234         {
235             if (!(currentCharacter == ' ' || currentCharacter == ';'))
236             {
237                 break;
238             }
239         }
240         else
241         {
242             result.emplace_back(numStr.toShort());
243             continue;
244         }
245         ++it;
246     }
247     return result;
248 }
249 
parseCSI(const QString & input,QString::const_iterator & it,QTextCharFormat & format,const QTextCharFormat & defaultFormat,QTextCursor & cursor)250 static void parseCSI(
251     const QString& input,
252     QString::const_iterator& it,
253     QTextCharFormat& format,
254     const QTextCharFormat& defaultFormat,
255     QTextCursor& cursor)
256 {
257     std::vector<unsigned char> numericArguments = parseNumericArguments(input, it);
258     char numericArgument = numericArguments.empty() ? 1 : numericArguments[0];
259     if (it != input.cend())
260     {
261         const auto code = (*it).unicode();
262         switch(code)
263         {
264             case 'A': // cursor up
265                 cursor.movePosition(QTextCursor::Up, QTextCursor::MoveAnchor, numericArgument);
266                 break;
267             case 'B': // cursor down
268                 cursor.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor, numericArgument);
269                 break;
270             case 'C': // cursor forward
271                 cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, numericArgument);
272                 break;
273             case 'D': // cursor back
274                 cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, numericArgument);
275                 break;
276             case 'E': // cursor next line
277                 cursor.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor, numericArgument);
278                 cursor.movePosition(QTextCursor::StartOfLine, QTextCursor::MoveAnchor);
279                 break;
280             case 'F': // cursor previous line
281                 cursor.movePosition(QTextCursor::Up, QTextCursor::MoveAnchor, numericArgument);
282                 cursor.movePosition(QTextCursor::StartOfLine, QTextCursor::MoveAnchor);
283                 break;
284             case 'G': // cursor horizontal absolute position set
285                 cursor.movePosition(QTextCursor::StartOfLine, QTextCursor::MoveAnchor);
286                 cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, numericArgument);
287                 break;
288             case 'H': // Set cursor position ( row;col )
289                 if (numericArguments.size() > 1)
290                 {
291                     cursor.setPosition(0, QTextCursor::MoveAnchor);
292                     cursor.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor, numericArguments[0]);
293                     cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, numericArguments[1]);
294                 }
295                 break;
296             case 'J': // Erase in display
297                 /// @TODO ANSI escape: implement erase in display.
298                 break;
299             case 'K': // Erase in line
300                 /// @TODO ANSI escape: implement erase in line.
301                 break;
302             case 'S': // Scroll up
303                 /// @TODO ANSI escape: implement scroll up.
304                 break;
305             case 'T': // Scroll down
306                 /// @TODO ANSI escape: implement scroll down.
307                 break;
308             case 'f': // Horizontal vertical position
309                 if (numericArguments.size() > 1)
310                 {
311                     cursor.setPosition(0, QTextCursor::MoveAnchor);
312                     cursor.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor, numericArguments[0]);
313                     cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, numericArguments[1]);
314                 }
315                 break;
316             case 'm': // SGR
317                 parseSGR(numericArguments, input, it, format, defaultFormat, cursor);
318                 break;
319             default:
320                 // Stuff we ignore: CSI 5i, CSI 4i, CSI 6n.
321                 // We do care about SGR (CSI n <m>)
322                 break;
323         }
324     }
325 }
326 
327 // Replaces CR-LF line endings with pure LF endings,
328 // and then replaces any remaining CRs with LFs.
cleanLineEndings(QString & input)329 static QString cleanLineEndings(QString &input)
330 {
331     // Replace all CR-LF with LF
332     input.replace("\r\n", "\n");
333 
334     // Replace stray CRs with LF
335     input.replace("\r", "\n");
336 
337     return input;
338 }
339 
renderEscapeCodes(const QByteArray & input,QTextCharFormat & format,const QTextCharFormat & defaultFormat,QTextCursor & cursor)340 void renderhelpers::renderEscapeCodes(const QByteArray &input,
341     QTextCharFormat& format, const QTextCharFormat& defaultFormat, QTextCursor& cursor)
342 {
343     QString inputString = QString::fromUtf8(input);
344     cleanLineEndings(inputString);
345 
346     // Don't render escapes if set to 'ignore'
347     if (kristall::globals().options.ansi_escapes == AnsiEscRenderMode::ignore)
348     {
349         cursor.insertText(input, defaultFormat);
350         return;
351     }
352 
353     const auto tokens = input.split(escapeString);
354     for (QString::const_iterator it = inputString.cbegin(); it != inputString.cend(); ++it)
355     {
356         const auto currentCharacter = *it;;
357         if (currentCharacter == escapeString)
358         {
359             it++;
360             const auto escSequence = *it;
361             if (escSequence == "[")
362             {
363                 it++;
364                 parseCSI(input, it, format, defaultFormat, cursor);
365             }
366         }
367         else if (kristall::globals().options.ansi_escapes == AnsiEscRenderMode::strip)
368         {
369             // 'strip' mode -> we still interpret escapes as above, but just render
370             // text in the default format.
371             cursor.insertText(currentCharacter, defaultFormat);
372         }
373         else
374         {
375             // 'render' mode -> we use the interpreted ANSI format
376             cursor.insertText(currentCharacter, format);
377         }
378     }
379 }
380 
replace_quotes(QByteArray & line)381 QByteArray renderhelpers::replace_quotes(QByteArray &line)
382 {
383     if (!kristall::globals().options.fancy_quotes)
384         return line;
385 
386     int last_d = -1,
387         last_s = -1;
388 
389     for (int i = 0; i < line.length(); ++i)
390     {
391         // Double quotes
392         if (line[i] == '"')
393         {
394             if (last_d == -1)
395             {
396                 last_d = i;
397             }
398             else
399             {
400                 // Replace quote at first position:
401                 QByteArray first = QString("“").toUtf8();
402                 line.replace(last_d, 1, first);
403 
404                 // Replace quote at second position:
405                 line.replace(i + first.size() - 1, 1, QString("”").toUtf8());
406 
407                 last_d = -1;
408             }
409         }
410         else if (line[i] == '\'')
411         {
412             if (last_s == -1)
413             {
414                 // Skip if it looks like a contraction rather
415                 // than a quote.
416                 if (i > 0 && line[i - 1] != ' ')
417                 {
418                     line.replace(i, 1, QString("’").toUtf8());
419                     continue;
420                 }
421 
422                 // For shortenings like 'till
423                 int len = line.length();
424                 if ((i + 1) < len && line[i + 1] != ' ')
425                 {
426                     line.replace(i, 1, QString("‘").toUtf8());
427                     continue;
428                 }
429 
430                 last_s = i;
431             }
432             else
433             {
434                 // Replace quote at first position:
435                 QByteArray first = QString("‘").toUtf8();
436                 line.replace(last_s, 1, first);
437 
438                 // Replace quote at second position:
439                 line.replace(i + first.size() - 1, 1, QString("’").toUtf8());
440 
441                 last_s = -1;
442             }
443         }
444     }
445 
446     return line;
447 }
448 
setPageMargins(QTextDocument * doc,int mh,int mv)449 void renderhelpers::setPageMargins(QTextDocument *doc, int mh, int mv)
450 {
451     QTextFrame *root = doc->rootFrame();
452     QTextFrameFormat fmt = root->frameFormat();
453     fmt.setLeftMargin(mh);
454     fmt.setRightMargin(mh);
455     fmt.setTopMargin(mv);
456     fmt.setBottomMargin(mv);
457     root->setFrameFormat(fmt);
458 }
459