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