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