2005-06-15 [colin] 1.9.11cvs75
[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                 if (tag->attr && tag->attr->data &&
423                     !strcmp(((HTMLAttr *)tag->attr->data)->name, "href")) {
424                         g_free(parser->href);
425                         parser->href =
426                                 g_strdup(((HTMLAttr *)tag->attr->data)->value);
427                         parser->state = HTML_HREF_BEG;
428                 }
429         } else if (!strcmp(tag->name, "/a")) {
430                 parser->state = HTML_HREF;
431         } else if (!strcmp(tag->name, "p")) {
432                 parser->space = FALSE;
433                 if (!parser->empty_line) {
434                         parser->space = FALSE;
435                         if (!parser->newline) html_append_char(parser, '\n');
436                         html_append_char(parser, '\n');
437                 }
438                 parser->state = HTML_PAR;
439         } else if (!strcmp(tag->name, "pre")) {
440                 parser->pre = TRUE;
441                 parser->state = HTML_PRE;
442         } else if (!strcmp(tag->name, "/pre")) {
443                 parser->pre = FALSE;
444                 parser->state = HTML_NORMAL;
445         } else if (!strcmp(tag->name, "hr")) {
446                 if (!parser->newline) {
447                         parser->space = FALSE;
448                         html_append_char(parser, '\n');
449                 }
450                 html_append_str(parser, HR_STR "\n", -1);
451                 parser->state = HTML_HR;
452         } else if (!strcmp(tag->name, "div")    ||
453                    !strcmp(tag->name, "ul")     ||
454                    !strcmp(tag->name, "li")     ||
455                    !strcmp(tag->name, "table")  ||
456                    !strcmp(tag->name, "tr")     ||
457                    (tag->name[0] == 'h' && g_ascii_isdigit(tag->name[1]))) {
458                 if (!parser->newline) {
459                         parser->space = FALSE;
460                         html_append_char(parser, '\n');
461                 }
462                 parser->state = HTML_NORMAL;
463         } else if (!strcmp(tag->name, "/table") ||
464                    (tag->name[0] == '/' &&
465                     tag->name[1] == 'h' &&
466                     g_ascii_isdigit(tag->name[1]))) {
467                 if (!parser->empty_line) {
468                         parser->space = FALSE;
469                         if (!parser->newline) html_append_char(parser, '\n');
470                         html_append_char(parser, '\n');
471                 }
472                 parser->state = HTML_NORMAL;
473         } else if (!strcmp(tag->name, "/div")   ||
474                    !strcmp(tag->name, "/ul")    ||
475                    !strcmp(tag->name, "/li")) {
476                 if (!parser->newline) {
477                         parser->space = FALSE;
478                         html_append_char(parser, '\n');
479                 }
480                 parser->state = HTML_NORMAL;
481                         }
482
483         html_free_tag(tag);
484
485         return parser->state;
486 }
487
488 static void html_parse_special(HTMLParser *parser)
489 {
490         gchar symbol_name[9];
491         gint n;
492         const gchar *val;
493
494         parser->state = HTML_UNKNOWN;
495         g_return_if_fail(*parser->bufp == '&');
496
497         /* &foo; */
498         for (n = 0; parser->bufp[n] != '\0' && parser->bufp[n] != ';'; n++)
499                 ;
500         if (n > 7 || parser->bufp[n] != ';') {
501                 /* output literal `&' */
502                 html_append_char(parser, *parser->bufp++);
503                 parser->state = HTML_NORMAL;
504                 return;
505         }
506         strncpy2(symbol_name, parser->bufp, n + 2);
507         parser->bufp += n + 1;
508
509         if ((val = g_hash_table_lookup(parser->symbol_table, symbol_name))
510             != NULL) {
511                 html_append_str(parser, val, -1);
512                 parser->state = HTML_NORMAL;
513                 return;
514         } else if (symbol_name[1] == '#' && g_ascii_isdigit(symbol_name[2])) {
515                 gint ch;
516
517                 ch = atoi(symbol_name + 2);
518                 if ((ch > 0 && ch <= 127) ||
519                     (ch >= 128 && ch <= 255 &&
520                      parser->conv->charset == C_ISO_8859_1)) {
521                         html_append_char(parser, ch);
522                         parser->state = HTML_NORMAL;
523                         return;
524                 } else {
525                         char *symb = NULL;
526                         switch (ch) {
527                         /* http://www.w3schools.com/html/html_entitiesref.asp */
528                         case 338:       /* capital ligature OE  &OElig;  */
529                                 symb = "OE";  
530                                 break;
531                         case 339:       /* small ligature OE    &oelig;  */
532                                 symb = "oe";  
533                                 break;
534                         case 352:       /* capital S w/caron    &Scaron; */
535                         case 353:       /* small S w/caron      &scaron; */
536                         case 376:       /* cap Y w/ diaeres     &Yuml;   */
537                                 break;
538                         case 710:       /* circumflex accent    &circ;   */
539                                 symb = "^";  
540                                 break;
541                         case 732:       /* small tilde          &tilde;  */
542                                 symb = "~";  
543                                 break;
544                         case 8194:      /* en space             &ensp;   */
545                         case 8195:      /* em space             &emsp;   */
546                         case 8201:      /* thin space           &thinsp; */
547                                 symb = " ";  
548                                 break;
549                         case 8204:      /* zero width non-joiner &zwnj;  */
550                         case 8205:      /* zero width joiner    &zwj;   */
551                         case 8206:      /* l-t-r mark           &lrm;   */
552                         case 8207:      /* r-t-l mark           &rlm     */
553                                 break;
554                         case 8211:      /* en dash              &ndash;  */
555                                 symb = "-";  
556                                 break;
557                         case 8212:      /* em dash              &mdash;  */
558                                 symb = "--";  
559                                 break;
560                         case 8216:      /* l single quot mark   &lsquo;  */
561                                 symb = "`";  
562                                 break;
563                         case 8217:      /* r single quot mark   &rsquo;  */
564                                 symb = "'";  
565                                 break;
566                         case 8218:      /* single low-9 quot    &sbquo;  */
567                                 symb = ",";  
568                                 break;
569                         case 8220:      /* l double quot mark   &ldquo;  */
570                                 symb = "``";  
571                                 break;
572                         case 8221:      /* r double quot mark   &rdquo;  */
573                                 symb = "''";  
574                                 break;
575                         case 8222:      /* double low-9 quot    &bdquo;  */
576                                 symb = ",,";  
577                                 break;
578                         case 8224:      /* dagger               &dagger; */
579                         case 8225:      /* double dagger        &Dagger; */
580                                 break;
581                         case 8230:      /* horizontal ellipsis  &hellip; */
582                                 symb = "...";  
583                                 break;
584                         case 8240:      /* per mile             &permil; */
585                                 symb = "\%o";  
586                                 break;
587                         case 8249:      /* l-pointing angle quot &lsaquo; */
588                                 symb = "<";  
589                                 break;
590                         case 8250:      /* r-pointing angle quot &rsaquo; */
591                                 symb = ">";  
592                                 break;
593                         case 8364:      /* euro                 &euro;   */
594                                 symb = "EUR";  
595                                 break;
596                         case 8482:      /* trademark            &trade;  */
597                                 symb  = "(TM)";  
598                                 break;
599                         default: 
600                                 break;
601                         }
602                         if (symb) {
603                                 html_append_str(parser, symb, -1);
604                                 parser->state = HTML_NORMAL;
605                                 return;
606                         }
607                 }
608         }
609
610         html_append_str(parser, symbol_name, -1);
611 }
612
613 static void html_get_parenthesis(HTMLParser *parser, gchar *buf, gint len)
614 {
615         gchar *p;
616
617         buf[0] = '\0';
618         g_return_if_fail(*parser->bufp == '<');
619
620         /* ignore comment / CSS / script stuff */
621         if (!strncmp(parser->bufp, "<!--", 4)) {
622                 parser->bufp += 4;
623                 while ((p = strstr(parser->bufp, "-->")) == NULL)
624                         if (html_read_line(parser) == HTML_EOF) return;
625                 parser->bufp = p + 3;
626                 return;
627         }
628         if (!g_ascii_strncasecmp(parser->bufp, "<style", 6)) {
629                 parser->bufp += 6;
630                 while ((p = strcasestr(parser->bufp, "</style>")) == NULL)
631                         if (html_read_line(parser) == HTML_EOF) return;
632                 parser->bufp = p + 8;
633                 return;
634         }
635         if (!g_ascii_strncasecmp(parser->bufp, "<script", 7)) {
636                 parser->bufp += 7;
637                 while ((p = strcasestr(parser->bufp, "</script>")) == NULL)
638                         if (html_read_line(parser) == HTML_EOF) return;
639                 parser->bufp = p + 9;
640                 return;
641         }
642
643         parser->bufp++;
644         while ((p = strchr(parser->bufp, '>')) == NULL)
645                 if (html_read_line(parser) == HTML_EOF) return;
646
647         strncpy2(buf, parser->bufp, MIN(p - parser->bufp + 1, len));
648         g_strstrip(buf);
649         parser->bufp = p + 1;
650 }