1 /**
2 * @file
3 * Rich text handler
4 *
5 * @authors
6 * Copyright (C) 2018 Richard Russon <rich@flatcap.org>
7 *
8 * @copyright
9 * This program is free software: you can redistribute it and/or modify it under
10 * the terms of the GNU General Public License as published by the Free Software
11 * Foundation, either version 2 of the License, or (at your option) any later
12 * version.
13 *
14 * This program is distributed in the hope that it will be useful, but WITHOUT
15 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 * details.
18 *
19 * You should have received a copy of the GNU General Public License along with
20 * this program. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23 /**
24 * @page neo_enriched Rich text handler
25 *
26 * Rich text handler
27 */
28
29 /**
30 * A (not so) minimal implementation of RFC1563.
31 */
32
33 #include "config.h"
34 #include <stddef.h>
35 #include <stdbool.h>
36 #include <stdio.h>
37 #include <wchar.h>
38 #include <wctype.h>
39 #include "mutt/lib.h"
40 #include "email/lib.h"
41 #include "enriched.h"
42
43 #define INDENT_SIZE 4
44
45 /**
46 * enum RichAttribs - Rich text attributes
47 */
48 enum RichAttribs
49 {
50 RICH_PARAM = 0, ///< Parameter label
51 RICH_BOLD, ///< Bold text
52 RICH_UNDERLINE, ///< Underlined text
53 RICH_ITALIC, ///< Italic text
54 RICH_NOFILL, ///< Text will not be reformatted
55 RICH_INDENT, ///< Indented text
56 RICH_INDENT_RIGHT, ///< Right-indented text
57 RICH_EXCERPT, ///< Excerpt text
58 RICH_CENTER, ///< Centred text
59 RICH_FLUSHLEFT, ///< Left-justified text
60 RICH_FLUSHRIGHT, ///< Right-justified text
61 RICH_COLOR, ///< Coloured text
62 RICH_MAX,
63 };
64
65 /**
66 * struct Etags - Enriched text tags
67 */
68 struct Etags
69 {
70 const wchar_t *tag_name; ///< Tag name
71 int index; ///< Index number
72 };
73
74 static const struct Etags EnrichedTags[] = {
75 // clang-format off
76 { L"param", RICH_PARAM },
77 { L"bold", RICH_BOLD },
78 { L"italic", RICH_ITALIC },
79 { L"underline", RICH_UNDERLINE },
80 { L"nofill", RICH_NOFILL },
81 { L"excerpt", RICH_EXCERPT },
82 { L"indent", RICH_INDENT },
83 { L"indentright", RICH_INDENT_RIGHT },
84 { L"center", RICH_CENTER },
85 { L"flushleft", RICH_FLUSHLEFT },
86 { L"flushright", RICH_FLUSHRIGHT },
87 { L"flushboth", RICH_FLUSHLEFT },
88 { L"color", RICH_COLOR },
89 { L"x-color", RICH_COLOR },
90 { NULL, -1 },
91 // clang-format on
92 };
93
94 /**
95 * struct EnrichedState - State of enriched-text parser
96 */
97 struct EnrichedState
98 {
99 wchar_t *buffer;
100 wchar_t *line;
101 wchar_t *param;
102 size_t buf_len;
103 size_t line_len;
104 size_t line_used;
105 size_t line_max;
106 size_t indent_len;
107 size_t word_len;
108 size_t buf_used;
109 size_t param_used;
110 size_t param_len;
111 int tag_level[RICH_MAX];
112 int wrap_margin;
113 struct State *s;
114 };
115
116 /**
117 * enriched_wrap - Wrap enriched text
118 * @param stte State of enriched text
119 */
enriched_wrap(struct EnrichedState * stte)120 static void enriched_wrap(struct EnrichedState *stte)
121 {
122 if (!stte)
123 return;
124
125 int x;
126
127 if (stte->line_len)
128 {
129 if (stte->tag_level[RICH_CENTER] || stte->tag_level[RICH_FLUSHRIGHT])
130 {
131 /* Strip trailing white space */
132 size_t y = stte->line_used - 1;
133
134 while (y && iswspace(stte->line[y]))
135 {
136 stte->line[y] = (wchar_t) '\0';
137 y--;
138 stte->line_used--;
139 stte->line_len--;
140 }
141 if (stte->tag_level[RICH_CENTER])
142 {
143 /* Strip leading whitespace */
144 y = 0;
145
146 while (stte->line[y] && iswspace(stte->line[y]))
147 y++;
148 if (y)
149 {
150 for (size_t z = y; z <= stte->line_used; z++)
151 {
152 stte->line[z - y] = stte->line[z];
153 }
154
155 stte->line_len -= y;
156 stte->line_used -= y;
157 }
158 }
159 }
160
161 const int extra = stte->wrap_margin - stte->line_len - stte->indent_len -
162 (stte->tag_level[RICH_INDENT_RIGHT] * INDENT_SIZE);
163 if (extra > 0)
164 {
165 if (stte->tag_level[RICH_CENTER])
166 {
167 x = extra / 2;
168 while (x)
169 {
170 state_putc(stte->s, ' ');
171 x--;
172 }
173 }
174 else if (stte->tag_level[RICH_FLUSHRIGHT])
175 {
176 x = extra - 1;
177 while (x)
178 {
179 state_putc(stte->s, ' ');
180 x--;
181 }
182 }
183 }
184 state_putws(stte->s, (const wchar_t *) stte->line);
185 }
186
187 state_putc(stte->s, '\n');
188 stte->line[0] = (wchar_t) '\0';
189 stte->line_len = 0;
190 stte->line_used = 0;
191 stte->indent_len = 0;
192 if (stte->s->prefix)
193 {
194 state_puts(stte->s, stte->s->prefix);
195 stte->indent_len += mutt_str_len(stte->s->prefix);
196 }
197
198 if (stte->tag_level[RICH_EXCERPT])
199 {
200 x = stte->tag_level[RICH_EXCERPT];
201 while (x)
202 {
203 if (stte->s->prefix)
204 {
205 state_puts(stte->s, stte->s->prefix);
206 stte->indent_len += mutt_str_len(stte->s->prefix);
207 }
208 else
209 {
210 state_puts(stte->s, "> ");
211 stte->indent_len += mutt_str_len("> ");
212 }
213 x--;
214 }
215 }
216 else
217 stte->indent_len = 0;
218 if (stte->tag_level[RICH_INDENT])
219 {
220 x = stte->tag_level[RICH_INDENT] * INDENT_SIZE;
221 stte->indent_len += x;
222 while (x)
223 {
224 state_putc(stte->s, ' ');
225 x--;
226 }
227 }
228 }
229
230 /**
231 * enriched_flush - Write enriched text to the State
232 * @param stte State of Enriched text
233 * @param wrap true if the text should be wrapped
234 */
enriched_flush(struct EnrichedState * stte,bool wrap)235 static void enriched_flush(struct EnrichedState *stte, bool wrap)
236 {
237 if (!stte || !stte->buffer)
238 return;
239
240 if (!stte->tag_level[RICH_NOFILL] &&
241 ((stte->line_len + stte->word_len) >
242 (stte->wrap_margin - (stte->tag_level[RICH_INDENT_RIGHT] * INDENT_SIZE) - stte->indent_len)))
243 {
244 enriched_wrap(stte);
245 }
246
247 if (stte->buf_used)
248 {
249 stte->buffer[stte->buf_used] = (wchar_t) '\0';
250 stte->line_used += stte->buf_used;
251 if (stte->line_used > stte->line_max)
252 {
253 stte->line_max = stte->line_used;
254 mutt_mem_realloc(&stte->line, (stte->line_max + 1) * sizeof(wchar_t));
255 }
256 wcscat(stte->line, stte->buffer);
257 stte->line_len += stte->word_len;
258 stte->word_len = 0;
259 stte->buf_used = 0;
260 }
261 if (wrap)
262 enriched_wrap(stte);
263 fflush(stte->s->fp_out);
264 }
265
266 /**
267 * enriched_putwc - Write one wide character to the state
268 * @param c Character to write
269 * @param stte State of Enriched text
270 */
enriched_putwc(wchar_t c,struct EnrichedState * stte)271 static void enriched_putwc(wchar_t c, struct EnrichedState *stte)
272 {
273 if (!stte)
274 return;
275
276 if (stte->tag_level[RICH_PARAM])
277 {
278 if (stte->tag_level[RICH_COLOR])
279 {
280 if (stte->param_used + 1 >= stte->param_len)
281 mutt_mem_realloc(&stte->param, (stte->param_len += 256) * sizeof(wchar_t));
282
283 stte->param[stte->param_used++] = c;
284 }
285 return; /* nothing to do */
286 }
287
288 /* see if more space is needed (plus extra for possible rich characters) */
289 if ((stte->buf_len < (stte->buf_used + 3)) || !stte->buffer)
290 {
291 stte->buf_len += 1024;
292 mutt_mem_realloc(&stte->buffer, (stte->buf_len + 1) * sizeof(wchar_t));
293 }
294
295 if ((!stte->tag_level[RICH_NOFILL] && iswspace(c)) || (c == (wchar_t) '\0'))
296 {
297 if (c == (wchar_t) '\t')
298 stte->word_len += 8 - (stte->line_len + stte->word_len) % 8;
299 else
300 stte->word_len++;
301
302 stte->buffer[stte->buf_used++] = c;
303 enriched_flush(stte, false);
304 }
305 else
306 {
307 if (stte->s->flags & MUTT_DISPLAY)
308 {
309 if (stte->tag_level[RICH_BOLD])
310 {
311 stte->buffer[stte->buf_used++] = c;
312 stte->buffer[stte->buf_used++] = (wchar_t) '\010'; // Ctrl-H (backspace)
313 stte->buffer[stte->buf_used++] = c;
314 }
315 else if (stte->tag_level[RICH_UNDERLINE])
316 {
317 stte->buffer[stte->buf_used++] = '_';
318 stte->buffer[stte->buf_used++] = (wchar_t) '\010'; // Ctrl-H (backspace)
319 stte->buffer[stte->buf_used++] = c;
320 }
321 else if (stte->tag_level[RICH_ITALIC])
322 {
323 stte->buffer[stte->buf_used++] = c;
324 stte->buffer[stte->buf_used++] = (wchar_t) '\010'; // Ctrl-H (backspace)
325 stte->buffer[stte->buf_used++] = '_';
326 }
327 else
328 {
329 stte->buffer[stte->buf_used++] = c;
330 }
331 }
332 else
333 {
334 stte->buffer[stte->buf_used++] = c;
335 }
336 stte->word_len++;
337 }
338 }
339
340 /**
341 * enriched_puts - Write an enriched text string to the State
342 * @param s String to write
343 * @param stte State of Enriched text
344 */
enriched_puts(const char * s,struct EnrichedState * stte)345 static void enriched_puts(const char *s, struct EnrichedState *stte)
346 {
347 if (!stte)
348 return;
349
350 const char *c = NULL;
351
352 if ((stte->buf_len < (stte->buf_used + mutt_str_len(s))) || !stte->buffer)
353 {
354 stte->buf_len += 1024;
355 mutt_mem_realloc(&stte->buffer, (stte->buf_len + 1) * sizeof(wchar_t));
356 }
357 c = s;
358 while (*c)
359 {
360 stte->buffer[stte->buf_used++] = (wchar_t) *c;
361 c++;
362 }
363 }
364
365 /**
366 * enriched_set_flags - Set flags on the enriched text state
367 * @param tag Tag to set
368 * @param stte State of Enriched text
369 */
enriched_set_flags(const wchar_t * tag,struct EnrichedState * stte)370 static void enriched_set_flags(const wchar_t *tag, struct EnrichedState *stte)
371 {
372 if (!stte)
373 return;
374
375 const wchar_t *tagptr = tag;
376 int i, j;
377
378 if (*tagptr == (wchar_t) '/')
379 tagptr++;
380
381 for (i = 0, j = -1; EnrichedTags[i].tag_name; i++)
382 {
383 if (wcscasecmp(EnrichedTags[i].tag_name, tagptr) == 0)
384 {
385 j = EnrichedTags[i].index;
386 break;
387 }
388 }
389
390 if (j != -1)
391 {
392 if ((j == RICH_CENTER) || (j == RICH_FLUSHLEFT) || (j == RICH_FLUSHRIGHT))
393 enriched_flush(stte, true);
394
395 if (*tag == (wchar_t) '/')
396 {
397 if (stte->tag_level[j]) /* make sure not to go negative */
398 stte->tag_level[j]--;
399 if ((stte->s->flags & MUTT_DISPLAY) && (j == RICH_PARAM) && stte->tag_level[RICH_COLOR])
400 {
401 stte->param[stte->param_used] = (wchar_t) '\0';
402 if (wcscasecmp(L"black", stte->param) == 0)
403 {
404 enriched_puts("\033[30m", stte); // Escape
405 }
406 else if (wcscasecmp(L"red", stte->param) == 0)
407 {
408 enriched_puts("\033[31m", stte); // Escape
409 }
410 else if (wcscasecmp(L"green", stte->param) == 0)
411 {
412 enriched_puts("\033[32m", stte); // Escape
413 }
414 else if (wcscasecmp(L"yellow", stte->param) == 0)
415 {
416 enriched_puts("\033[33m", stte); // Escape
417 }
418 else if (wcscasecmp(L"blue", stte->param) == 0)
419 {
420 enriched_puts("\033[34m", stte); // Escape
421 }
422 else if (wcscasecmp(L"magenta", stte->param) == 0)
423 {
424 enriched_puts("\033[35m", stte); // Escape
425 }
426 else if (wcscasecmp(L"cyan", stte->param) == 0)
427 {
428 enriched_puts("\033[36m", stte); // Escape
429 }
430 else if (wcscasecmp(L"white", stte->param) == 0)
431 {
432 enriched_puts("\033[37m", stte); // Escape
433 }
434 }
435 if ((stte->s->flags & MUTT_DISPLAY) && (j == RICH_COLOR))
436 {
437 enriched_puts("\033[0m", stte); // Escape
438 }
439
440 /* flush parameter buffer when closing the tag */
441 if (j == RICH_PARAM)
442 {
443 stte->param_used = 0;
444 stte->param[0] = (wchar_t) '\0';
445 }
446 }
447 else
448 stte->tag_level[j]++;
449
450 if (j == RICH_EXCERPT)
451 enriched_flush(stte, true);
452 }
453 }
454
455 /**
456 * text_enriched_handler - Handler for enriched text - Implements ::handler_t - @ingroup handler_api
457 * @retval 0 Always
458 */
text_enriched_handler(struct Body * a,struct State * s)459 int text_enriched_handler(struct Body *a, struct State *s)
460 {
461 enum
462 {
463 TEXT,
464 LANGLE,
465 TAG,
466 BOGUS_TAG,
467 NEWLINE,
468 ST_EOF,
469 DONE
470 } state = TEXT;
471
472 long bytes = a->length;
473 struct EnrichedState stte = { 0 };
474 wint_t wc = 0;
475 int tag_len = 0;
476 wchar_t tag[1024 + 1];
477
478 stte.s = s;
479 stte.wrap_margin =
480 ((s->wraplen > 4) && ((s->flags & MUTT_DISPLAY) || (s->wraplen < 76))) ?
481 s->wraplen - 4 :
482 72;
483 stte.line_max = stte.wrap_margin * 4;
484 stte.line = mutt_mem_calloc((stte.line_max + 1), sizeof(wchar_t));
485 stte.param = mutt_mem_calloc(256, sizeof(wchar_t));
486
487 stte.param_len = 256;
488 stte.param_used = 0;
489
490 if (s->prefix)
491 {
492 state_puts(s, s->prefix);
493 stte.indent_len += mutt_str_len(s->prefix);
494 }
495
496 while (state != DONE)
497 {
498 if (state != ST_EOF)
499 {
500 if (!bytes || ((wc = fgetwc(s->fp_in)) == WEOF))
501 state = ST_EOF;
502 else
503 bytes--;
504 }
505
506 switch (state)
507 {
508 case TEXT:
509 switch (wc)
510 {
511 case '<':
512 state = LANGLE;
513 break;
514
515 case '\n':
516 if (stte.tag_level[RICH_NOFILL])
517 {
518 enriched_flush(&stte, true);
519 }
520 else
521 {
522 enriched_putwc((wchar_t) ' ', &stte);
523 state = NEWLINE;
524 }
525 break;
526
527 default:
528 enriched_putwc(wc, &stte);
529 }
530 break;
531
532 case LANGLE:
533 if (wc == (wchar_t) '<')
534 {
535 enriched_putwc(wc, &stte);
536 state = TEXT;
537 break;
538 }
539 else
540 {
541 tag_len = 0;
542 state = TAG;
543 }
544 /* Yes, (it wasn't a <<, so this char is first in TAG) */
545 /* fallthrough */
546 case TAG:
547 if (wc == (wchar_t) '>')
548 {
549 tag[tag_len] = (wchar_t) '\0';
550 enriched_set_flags(tag, &stte);
551 state = TEXT;
552 }
553 else if (tag_len < 1024) /* ignore overly long tags */
554 tag[tag_len++] = wc;
555 else
556 state = BOGUS_TAG;
557 break;
558
559 case BOGUS_TAG:
560 if (wc == (wchar_t) '>')
561 state = TEXT;
562 break;
563
564 case NEWLINE:
565 if (wc == (wchar_t) '\n')
566 enriched_flush(&stte, true);
567 else
568 {
569 ungetwc(wc, s->fp_in);
570 bytes++;
571 state = TEXT;
572 }
573 break;
574
575 case ST_EOF:
576 enriched_putwc((wchar_t) '\0', &stte);
577 enriched_flush(&stte, true);
578 state = DONE;
579 break;
580
581 default:
582 /* not reached */
583 break;
584 }
585 }
586
587 state_putc(s, '\n'); /* add a final newline */
588
589 FREE(&(stte.buffer));
590 FREE(&(stte.line));
591 FREE(&(stte.param));
592
593 return 0;
594 }
595