2005-07-13 [paul] 1.9.12cvs44
[claws.git] / src / html.c
1 /*
2  * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999-2005 Hiroyuki Yamamoto
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18  */
19
20 #include <glib.h>
21 #include <stdio.h>
22 #include <string.h>
23 #include <ctype.h>
24
25 #include "html.h"
26 #include "codeconv.h"
27 #include "utils.h"
28
29 #define HTMLBUFSIZE     8192
30 #define HR_STR          "------------------------------------------------"
31
32 typedef struct _HTMLSymbol      HTMLSymbol;
33
34 struct _HTMLSymbol
35 {
36         gchar *const key;
37         gchar *const val;
38 };
39
40 static HTMLSymbol symbol_list[] = {
41         {"&lt;"    , "<"},
42         {"&gt;"    , ">"},
43         {"&amp;"   , "&"},
44         {"&quot;"  , "\""},
45         {"&nbsp;"  , " "},
46         {"&trade;" , "(TM)"},
47
48         {"&#153;", "(TM)"},
49 };
50
51 static HTMLSymbol ascii_symbol_list[] = {
52         {"&iexcl;" , "^!"},
53         {"&brvbar;", "|"},
54         {"&copy;"  , "(C)"},
55         {"&laquo;" , "<<"},
56         {"&reg;"   , "(R)"},
57
58         {"&sup2;"  , "^2"},
59         {"&sup3;"  , "^3"},
60         {"&acute;" , "'"},
61         {"&cedil;" , ","},
62         {"&sup1;"  , "^1"},
63         {"&raquo;" , ">>"},
64         {"&frac14;", "1/4"},
65         {"&frac12;", "1/2"},
66         {"&frac34;", "3/4"},
67         {"&iquest;", "^?"},
68
69         {"&Agrave;", "A`"},
70         {"&Aacute;", "A'"},
71         {"&Acirc;" , "A^"},
72         {"&Atilde;", "A~"},
73         {"&AElig;" , "AE"},
74         {"&Egrave;", "E`"},
75         {"&Eacute;", "E'"},
76         {"&Ecirc;" , "E^"},
77         {"&Igrave;", "I`"},
78         {"&Iacute;", "I'"},
79         {"&Icirc;" , "I^"},
80
81         {"&Ntilde;", "N~"},
82         {"&Ograve;", "O`"},
83         {"&Oacute;", "O'"},
84         {"&Ocirc;" , "O^"},
85         {"&Otilde;", "O~"},
86         {"&Ugrave;", "U`"},
87         {"&Uacute;", "U'"},
88         {"&Ucirc;" , "U^"},
89         {"&Yacute;", "Y'"},
90
91         {"&agrave;", "a`"},
92         {"&aacute;", "a'"},
93         {"&acirc;" , "a^"},
94         {"&atilde;", "a~"},
95         {"&aelig;" , "ae"},
96         {"&egrave;", "e`"},
97         {"&eacute;", "e'"},
98         {"&ecirc;" , "e^"},
99         {"&igrave;", "i`"},
100         {"&iacute;", "i'"},
101         {"&icirc;" , "i^"},
102
103         {"&ntilde;", "n~"},
104         {"&ograve;", "o`"},
105         {"&oacute;", "o'"},
106         {"&ocirc;" , "o^"},
107         {"&otilde;", "o~"},
108         {"&ugrave;", "u`"},
109         {"&uacute;", "u'"},
110         {"&ucirc;" , "u^"},
111         {"&yacute;", "y'"},
112 };
113
114 static GHashTable *default_symbol_table;
115
116 static HTMLState html_read_line         (HTMLParser     *parser);
117 static void html_append_char            (HTMLParser     *parser,
118                                          gchar           ch);
119 static void html_append_str             (HTMLParser     *parser,
120                                          const gchar    *str,
121                                          gint            len);
122 static HTMLState html_parse_tag         (HTMLParser     *parser);
123 static void html_parse_special          (HTMLParser     *parser);
124 static void html_get_parenthesis        (HTMLParser     *parser,
125                                          gchar          *buf,
126                                          gint            len);
127
128
129 HTMLParser *html_parser_new(FILE *fp, CodeConverter *conv)
130 {
131         HTMLParser *parser;
132
133         g_return_val_if_fail(fp != NULL, NULL);
134         g_return_val_if_fail(conv != NULL, NULL);
135
136         parser = g_new0(HTMLParser, 1);
137         parser->fp = fp;
138         parser->conv = conv;
139         parser->str = g_string_new(NULL);
140         parser->buf = g_string_new(NULL);
141         parser->bufp = parser->buf->str;
142         parser->state = HTML_NORMAL;
143         parser->href = NULL;
144         parser->newline = TRUE;
145         parser->empty_line = TRUE;
146         parser->space = FALSE;
147         parser->pre = FALSE;
148
149 #define SYMBOL_TABLE_ADD(table, list) \
150 { \
151         gint i; \
152  \
153         for (i = 0; i < sizeof(list) / sizeof(list[0]); i++) \
154                 g_hash_table_insert(table, list[i].key, list[i].val); \
155 }
156
157         if (!default_symbol_table) {
158                 default_symbol_table =
159                         g_hash_table_new(g_str_hash, g_str_equal);
160                 SYMBOL_TABLE_ADD(default_symbol_table, symbol_list);
161                 SYMBOL_TABLE_ADD(default_symbol_table, ascii_symbol_list);
162         }
163
164 #undef SYMBOL_TABLE_ADD
165
166         parser->symbol_table = default_symbol_table;
167
168         return parser;
169 }
170
171 void html_parser_destroy(HTMLParser *parser)
172 {
173         g_string_free(parser->str, TRUE);
174         g_string_free(parser->buf, TRUE);
175         g_free(parser->href);
176         g_free(parser);
177 }
178
179 gchar *html_parse(HTMLParser *parser)
180 {
181         parser->state = HTML_NORMAL;
182         g_string_truncate(parser->str, 0);
183
184         if (*parser->bufp == '\0') {
185                 g_string_truncate(parser->buf, 0);
186                 parser->bufp = parser->buf->str;
187                 if (html_read_line(parser) == HTML_EOF)
188                         return NULL;
189         }
190
191         while (*parser->bufp != '\0') {
192                 switch (*parser->bufp) {
193                 case '<': {
194                         HTMLState st;
195                         st = html_parse_tag(parser);
196                         /* when we see an href, we need to flush the str
197                          * buffer.  Then collect all the chars until we
198                          * see the end anchor tag
199                          */
200                         if (HTML_HREF_BEG == st || HTML_HREF == st)
201                                 return parser->str->str;
202                         } 
203                         break;
204                 case '&':
205                         html_parse_special(parser);
206                         break;
207                 case ' ':
208                 case '\t':
209                 case '\r':
210                 case '\n':
211                         if (parser->bufp[0] == '\r' && parser->bufp[1] == '\n')
212                                 parser->bufp++;
213
214                         if (!parser->pre) {
215                                 if (!parser->newline)
216                                         parser->space = TRUE;
217
218                                 parser->bufp++;
219                                 break;
220                         }
221                         /* fallthrough */
222                 default:
223                         html_append_char(parser, *parser->bufp++);
224                 }
225         }
226
227         return parser->str->str;
228 }
229
230 static HTMLState html_read_line(HTMLParser *parser)
231 {
232         gchar buf[HTMLBUFSIZE];
233         gchar buf2[HTMLBUFSIZE];
234         gint index;
235
236         if (fgets(buf, sizeof(buf), parser->fp) == NULL) {
237                 parser->state = HTML_EOF;
238                 return HTML_EOF;
239         }
240
241         if (conv_convert(parser->conv, buf2, sizeof(buf2), buf) < 0) {
242                 index = parser->bufp - parser->buf->str;
243
244                 conv_utf8todisp(buf2, sizeof(buf2), buf);
245                 g_string_append(parser->buf, buf2);
246
247                 parser->bufp = parser->buf->str + index;
248
249                 return HTML_CONV_FAILED;
250         }
251
252         index = parser->bufp - parser->buf->str;
253
254         g_string_append(parser->buf, buf2);
255
256         parser->bufp = parser->buf->str + index;
257
258         return HTML_NORMAL;
259 }
260
261 static void html_append_char(HTMLParser *parser, gchar ch)
262 {
263         GString *str = parser->str;
264
265         if (!parser->pre && parser->space) {
266                 g_string_append_c(str, ' ');
267                 parser->space = FALSE;
268         }
269
270         g_string_append_c(str, ch);
271
272         parser->empty_line = FALSE;
273         if (ch == '\n') {
274                 parser->newline = TRUE;
275                 if (str->len > 1 && str->str[str->len - 2] == '\n')
276                         parser->empty_line = TRUE;
277         } else
278                 parser->newline = FALSE;
279 }
280
281 static void html_append_str(HTMLParser *parser, const gchar *str, gint len)
282 {
283         GString *string = parser->str;
284
285         if (!parser->pre && parser->space) {
286                 g_string_append_c(string, ' ');
287                 parser->space = FALSE;
288         }
289
290         if (len == 0) return;
291         if (len < 0)
292                 g_string_append(string, str);
293         else {
294                 gchar *s;
295                 Xstrndup_a(s, str, len, return);
296                 g_string_append(string, s);
297         }
298
299         parser->empty_line = FALSE;
300         if (string->len > 0 && string->str[string->len - 1] == '\n') {
301                 parser->newline = TRUE;
302                 if (string->len > 1 && string->str[string->len - 2] == '\n')
303                         parser->empty_line = TRUE;
304         } else
305                 parser->newline = FALSE;
306 }
307
308 static HTMLTag *html_get_tag(const gchar *str)
309 {
310         HTMLTag *tag;
311         gchar *tmp;
312         guchar *tmpp;
313
314         g_return_val_if_fail(str != NULL, NULL);
315
316         if (*str == '\0' || *str == '!') return NULL;
317
318         Xstrdup_a(tmp, str, return NULL);
319
320         tag = g_new0(HTMLTag, 1);
321
322         for (tmpp = tmp; *tmpp != '\0' && !g_ascii_isspace(*tmpp); tmpp++)
323                 ;
324
325         if (*tmpp == '\0') {
326                 g_strdown(tmp);
327                 tag->name = g_strdup(tmp);
328                 return tag;
329         } else {
330                 *tmpp++ = '\0';
331                 g_strdown(tmp);
332                 tag->name = g_strdup(tmp);
333         }
334
335         while (*tmpp != '\0') {
336                 HTMLAttr *attr;
337                 gchar *attr_name;
338                 gchar *attr_value;
339                 gchar *p;
340                 gchar quote;
341
342                 while (g_ascii_isspace(*tmpp)) tmpp++;
343                 attr_name = tmpp;
344
345                 while (*tmpp != '\0' && !g_ascii_isspace(*tmpp) &&
346                        *tmpp != '=')
347                         tmpp++;
348                 if (*tmpp != '\0' && *tmpp != '=') {
349                         *tmpp++ = '\0';
350                         while (g_ascii_isspace(*tmpp)) tmpp++;
351                 }
352
353                 if (*tmpp == '=') {
354                         *tmpp++ = '\0';
355                         while (g_ascii_isspace(*tmpp)) tmpp++;
356
357                         if (*tmpp == '"' || *tmpp == '\'') {
358                                 /* name="value" */
359                                 quote = *tmpp;
360                                 tmpp++;
361                                 attr_value = tmpp;
362                                 if ((p = strchr(attr_value, quote)) == NULL) {
363                                         g_warning("html_get_tag(): syntax error in tag: '%s'\n", str);
364                                         return tag;
365                                 }
366                                 tmpp = p;
367                                 *tmpp++ = '\0';
368                                 while (g_ascii_isspace(*tmpp)) tmpp++;
369                         } else {
370                                 /* name=value */
371                                 attr_value = tmpp;
372                                 while (*tmpp != '\0' && !g_ascii_isspace(*tmpp)) tmpp++;
373                                 if (*tmpp != '\0')
374                                         *tmpp++ = '\0';
375                         }
376                 } else
377                         attr_value = "";
378
379                 g_strchomp(attr_name);
380                 g_strdown(attr_name);
381                 attr = g_new(HTMLAttr, 1);
382                 attr->name = g_strdup(attr_name);
383                 attr->value = g_strdup(attr_value);
384                 tag->attr = g_list_append(tag->attr, attr);
385         }
386
387         return tag;
388 }
389
390 static void html_free_tag(HTMLTag *tag)
391 {
392         if (!tag) return;
393
394         g_free(tag->name);
395         while (tag->attr != NULL) {
396                 HTMLAttr *attr = (HTMLAttr *)tag->attr->data;
397                 g_free(attr->name);
398                 g_free(attr->value);
399                 g_free(attr);
400                 tag->attr = g_list_remove(tag->attr, tag->attr->data);
401         }
402         g_free(tag);
403 }
404
405 static HTMLState html_parse_tag(HTMLParser *parser)
406 {
407         gchar buf[HTMLBUFSIZE];
408         HTMLTag *tag;
409
410         html_get_parenthesis(parser, buf, sizeof(buf));
411
412         tag = html_get_tag(buf);
413
414         parser->state = HTML_UNKNOWN;
415         if (!tag) return HTML_UNKNOWN;
416
417         if (!strcmp(tag->name, "br")) {
418                 parser->space = FALSE;
419                 html_append_char(parser, '\n');
420                 parser->state = HTML_BR;
421         } else if (!strcmp(tag->name, "a")) {
422                 GList *cur;
423                 for (cur = tag->attr; cur != NULL; cur = cur->next) {
424                         if (cur->data && !strcmp(((HTMLAttr *)cur->data)->name, "href")) {
425                                 g_free(parser->href);
426                                 parser->href = g_strdup(((HTMLAttr *)cur->data)->value);
427                                 parser->state = HTML_HREF_BEG;
428                                 break;
429                         }
430                 }
431         } else if (!strcmp(tag->name, "/a")) {
432                 parser->state = HTML_HREF;
433         } else if (!strcmp(tag->name, "p")) {
434                 parser->space = FALSE;
435                 if (!parser->empty_line) {
436                         parser->space = FALSE;
437                         if (!parser->newline) html_append_char(parser, '\n');
438                         html_append_char(parser, '\n');
439                 }
440                 parser->state = HTML_PAR;
441         } else if (!strcmp(tag->name, "pre")) {
442                 parser->pre = TRUE;
443                 parser->state = HTML_PRE;
444         } else if (!strcmp(tag->name, "/pre")) {
445                 parser->pre = FALSE;
446                 parser->state = HTML_NORMAL;
447         } else if (!strcmp(tag->name, "hr")) {
448                 if (!parser->newline) {
449                         parser->space = FALSE;
450                         html_append_char(parser, '\n');
451                 }
452                 html_append_str(parser, HR_STR "\n", -1);
453                 parser->state = HTML_HR;
454         } else if (!strcmp(tag->name, "div")    ||
455                    !strcmp(tag->name, "ul")     ||
456                    !strcmp(tag->name, "li")     ||
457                    !strcmp(tag->name, "table")  ||
458                    !strcmp(tag->name, "tr")     ||
459                    (tag->name[0] == 'h' && g_ascii_isdigit(tag->name[1]))) {
460                 if (!parser->newline) {
461                         parser->space = FALSE;
462                         html_append_char(parser, '\n');
463                 }
464                 parser->state = HTML_NORMAL;
465         } else if (!strcmp(tag->name, "/table") ||
466                    (tag->name[0] == '/' &&
467                     tag->name[1] == 'h' &&
468                     g_ascii_isdigit(tag->name[1]))) {
469                 if (!parser->empty_line) {
470                         parser->space = FALSE;
471                         if (!parser->newline) html_append_char(parser, '\n');
472                         html_append_char(parser, '\n');
473                 }
474                 parser->state = HTML_NORMAL;
475         } else if (!strcmp(tag->name, "/div")   ||
476                    !strcmp(tag->name, "/ul")    ||
477                    !strcmp(tag->name, "/li")) {
478                 if (!parser->newline) {
479                         parser->space = FALSE;
480                         html_append_char(parser, '\n');
481                 }
482                 parser->state = HTML_NORMAL;
483                         }
484
485         html_free_tag(tag);
486
487         return parser->state;
488 }
489
490 static void html_parse_special(HTMLParser *parser)
491 {
492         gchar symbol_name[9];
493         gint n;
494         const gchar *val;
495
496         parser->state = HTML_UNKNOWN;
497         g_return_if_fail(*parser->bufp == '&');
498
499         /* &foo; */
500         for (n = 0; parser->bufp[n] != '\0' && parser->bufp[n] != ';'; n++)
501                 ;
502         if (n > 7 || parser->bufp[n] != ';') {
503                 /* output literal `&' */
504                 html_append_char(parser, *parser->bufp++);
505                 parser->state = HTML_NORMAL;
506                 return;
507         }
508         strncpy2(symbol_name, parser->bufp, n + 2);
509         parser->bufp += n + 1;
510
511         if ((val = g_hash_table_lookup(parser->symbol_table, symbol_name))
512             != NULL) {
513                 html_append_str(parser, val, -1);
514                 parser->state = HTML_NORMAL;
515                 return;
516         } else if (symbol_name[1] == '#' && g_ascii_isdigit(symbol_name[2])) {
517                 gint ch;
518
519                 ch = atoi(symbol_name + 2);
520                 if ((ch > 0 && ch <= 127) ||
521                     (ch >= 128 && ch <= 255 &&
522                      parser->conv->charset == C_ISO_8859_1)) {
523                         html_append_char(parser, ch);
524                         parser->state = HTML_NORMAL;
525                         return;
526                 } else {
527                         char *symb = NULL;
528                         switch (ch) {
529                         /* http://www.w3schools.com/html/html_entitiesref.asp */
530                         case 338:       /* capital ligature OE  &OElig;  */
531                                 symb = "OE";  
532                                 break;
533                         case 339:       /* small ligature OE    &oelig;  */
534                                 symb = "oe";  
535                                 break;
536                         case 352:       /* capital S w/caron    &Scaron; */
537                         case 353:       /* small S w/caron      &scaron; */
538                         case 376:       /* cap Y w/ diaeres     &Yuml;   */
539                                 break;
540                         case 710:       /* circumflex accent    &circ;   */
541                                 symb = "^";  
542                                 break;
543                         case 732:       /* small tilde          &tilde;  */
544                                 symb = "~";  
545                                 break;
546                         case 8194:      /* en space             &ensp;   */
547                         case 8195:      /* em space             &emsp;   */
548                         case 8201:      /* thin space           &thinsp; */
549                                 symb = " ";  
550                                 break;
551                         case 8204:      /* zero width non-joiner &zwnj;  */
552                         case 8205:      /* zero width joiner    &zwj;   */
553                         case 8206:      /* l-t-r mark           &lrm;   */
554                         case 8207:      /* r-t-l mark           &rlm     */
555                                 break;
556                         case 8211:      /* en dash              &ndash;  */
557                                 symb = "-";  
558                                 break;
559                         case 8212:      /* em dash              &mdash;  */
560                                 symb = "--";  
561                                 break;
562                         case 8216:      /* l single quot mark   &lsquo;  */
563                                 symb = "`";  
564                                 break;
565                         case 8217:      /* r single quot mark   &rsquo;  */
566                                 symb = "'";  
567                                 break;
568                         case 8218:      /* single low-9 quot    &sbquo;  */
569                                 symb = ",";  
570                                 break;
571                         case 8220:      /* l double quot mark   &ldquo;  */
572                                 symb = "``";  
573                                 break;
574                         case 8221:      /* r double quot mark   &rdquo;  */
575                                 symb = "''";  
576                                 break;
577                         case 8222:      /* double low-9 quot    &bdquo;  */
578                                 symb = ",,";  
579                                 break;
580                         case 8224:      /* dagger               &dagger; */
581                         case 8225:      /* double dagger        &Dagger; */
582                                 break;
583                         case 8230:      /* horizontal ellipsis  &hellip; */
584                                 symb = "...";  
585                                 break;
586                         case 8240:      /* per mile             &permil; */
587                                 symb = "\%o";  
588                                 break;
589                         case 8249:      /* l-pointing angle quot &lsaquo; */
590                                 symb = "<";  
591                                 break;
592                         case 8250:      /* r-pointing angle quot &rsaquo; */
593                                 symb = ">";  
594                                 break;
595                         case 8364:      /* euro                 &euro;   */
596                                 symb = "EUR";  
597                                 break;
598                         case 8482:      /* trademark            &trade;  */
599                                 symb  = "(TM)";  
600                                 break;
601                         default: 
602                                 break;
603                         }
604                         if (symb) {
605                                 html_append_str(parser, symb, -1);
606                                 parser->state = HTML_NORMAL;
607                                 return;
608                         }
609                 }
610         }
611
612         html_append_str(parser, symbol_name, -1);
613 }
614
615 static void html_get_parenthesis(HTMLParser *parser, gchar *buf, gint len)
616 {
617         gchar *p;
618
619         buf[0] = '\0';
620         g_return_if_fail(*parser->bufp == '<');
621
622         /* ignore comment / CSS / script stuff */
623         if (!strncmp(parser->bufp, "<!--", 4)) {
624                 parser->bufp += 4;
625                 while ((p = strstr(parser->bufp, "-->")) == NULL)
626                         if (html_read_line(parser) == HTML_EOF) return;
627                 parser->bufp = p + 3;
628                 return;
629         }
630         if (!g_ascii_strncasecmp(parser->bufp, "<style", 6)) {
631                 parser->bufp += 6;
632                 while ((p = strcasestr(parser->bufp, "</style>")) == NULL)
633                         if (html_read_line(parser) == HTML_EOF) return;
634                 parser->bufp = p + 8;
635                 return;
636         }
637         if (!g_ascii_strncasecmp(parser->bufp, "<script", 7)) {
638                 parser->bufp += 7;
639                 while ((p = strcasestr(parser->bufp, "</script>")) == NULL)
640                         if (html_read_line(parser) == HTML_EOF) return;
641                 parser->bufp = p + 9;
642                 return;
643         }
644
645         parser->bufp++;
646         while ((p = strchr(parser->bufp, '>')) == NULL)
647                 if (html_read_line(parser) == HTML_EOF) return;
648
649         strncpy2(buf, parser->bufp, MIN(p - parser->bufp + 1, len));
650         g_strstrip(buf);
651         parser->bufp = p + 1;
652 }