html.c (12953B)
1 /* 2 * Claws Mail -- a GTK based, lightweight, and fast e-mail client 3 * Copyright (C) 1999-2016 Hiroyuki Yamamoto and the Claws Mail team 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 3 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, see <http://www.gnu.org/licenses/>. 17 */ 18 19 #include <glib.h> 20 #include <stdio.h> 21 #include <string.h> 22 #include <ctype.h> 23 24 #include "html.h" 25 #include "codeconv.h" 26 #include "utils.h" 27 #include "entity.h" 28 29 #define SC_HTMLBUFSIZE 8192 30 #define HR_STR "────────────────────────────────────────────────" 31 #define LI_STR "• " 32 33 static SC_HTMLState sc_html_read_line (SC_HTMLParser *parser); 34 static void sc_html_append_char (SC_HTMLParser *parser, 35 gchar ch); 36 static void sc_html_append_str (SC_HTMLParser *parser, 37 const gchar *str, 38 gint len); 39 static SC_HTMLState sc_html_parse_tag (SC_HTMLParser *parser); 40 static void sc_html_parse_special (SC_HTMLParser *parser); 41 static void sc_html_get_parenthesis (SC_HTMLParser *parser, 42 gchar *buf, 43 gint len); 44 45 46 SC_HTMLParser *sc_html_parser_new(FILE *fp, CodeConverter *conv) 47 { 48 SC_HTMLParser *parser; 49 50 cm_return_val_if_fail(fp != NULL, NULL); 51 cm_return_val_if_fail(conv != NULL, NULL); 52 53 parser = g_new0(SC_HTMLParser, 1); 54 parser->fp = fp; 55 parser->conv = conv; 56 parser->str = g_string_new(NULL); 57 parser->buf = g_string_new(NULL); 58 parser->bufp = parser->buf->str; 59 parser->state = SC_HTML_NORMAL; 60 parser->href = NULL; 61 parser->newline = TRUE; 62 parser->empty_line = TRUE; 63 parser->space = FALSE; 64 parser->pre = FALSE; 65 parser->indent = 0; 66 67 return parser; 68 } 69 70 void sc_html_parser_destroy(SC_HTMLParser *parser) 71 { 72 g_string_free(parser->str, TRUE); 73 g_string_free(parser->buf, TRUE); 74 g_free(parser->href); 75 g_free(parser); 76 } 77 78 gchar *sc_html_parse(SC_HTMLParser *parser) 79 { 80 parser->state = SC_HTML_NORMAL; 81 g_string_truncate(parser->str, 0); 82 83 if (*parser->bufp == '\0') { 84 g_string_truncate(parser->buf, 0); 85 parser->bufp = parser->buf->str; 86 if (sc_html_read_line(parser) == SC_HTML_EOF) 87 return NULL; 88 } 89 90 while (*parser->bufp != '\0') { 91 switch (*parser->bufp) { 92 case '<': { 93 SC_HTMLState st; 94 st = sc_html_parse_tag(parser); 95 /* when we see an href, we need to flush the str 96 * buffer. Then collect all the chars until we 97 * see the end anchor tag 98 */ 99 if (SC_HTML_HREF_BEG == st || SC_HTML_HREF == st) 100 return parser->str->str; 101 } 102 break; 103 case '&': 104 sc_html_parse_special(parser); 105 break; 106 case ' ': 107 case '\t': 108 case '\r': 109 case '\n': 110 if (parser->bufp[0] == '\r' && parser->bufp[1] == '\n') 111 parser->bufp++; 112 113 if (!parser->pre) { 114 if (!parser->newline) 115 parser->space = TRUE; 116 117 parser->bufp++; 118 break; 119 } 120 /* fallthrough */ 121 default: 122 sc_html_append_char(parser, *parser->bufp++); 123 } 124 } 125 126 return parser->str->str; 127 } 128 129 static SC_HTMLState sc_html_read_line(SC_HTMLParser *parser) 130 { 131 gchar buf[SC_HTMLBUFSIZE]; 132 gchar buf2[SC_HTMLBUFSIZE*4]; 133 gint index; 134 gint n; 135 136 if (parser->fp == NULL) 137 return SC_HTML_EOF; 138 139 n = fread(buf, 1, sizeof(buf) - 1, parser->fp); 140 if (n == 0) { 141 parser->state = SC_HTML_EOF; 142 return SC_HTML_EOF; 143 } else 144 buf[n] = '\0'; 145 146 if (conv_convert(parser->conv, buf2, sizeof(buf2), buf) < 0) { 147 index = parser->bufp - parser->buf->str; 148 149 conv_utf8todisp(buf2, sizeof(buf2), buf); 150 g_string_append(parser->buf, buf2); 151 152 parser->bufp = parser->buf->str + index; 153 154 return SC_HTML_CONV_FAILED; 155 } 156 157 index = parser->bufp - parser->buf->str; 158 159 g_string_append(parser->buf, buf2); 160 161 parser->bufp = parser->buf->str + index; 162 163 return SC_HTML_NORMAL; 164 } 165 166 static void sc_html_append_char(SC_HTMLParser *parser, gchar ch) 167 { 168 GString *str = parser->str; 169 170 if (!parser->pre && parser->space) { 171 g_string_append_c(str, ' '); 172 parser->space = FALSE; 173 } 174 175 g_string_append_c(str, ch); 176 177 parser->empty_line = FALSE; 178 if (ch == '\n') { 179 parser->newline = TRUE; 180 if (str->len > 1 && str->str[str->len - 2] == '\n') 181 parser->empty_line = TRUE; 182 if (parser->indent > 0) { 183 gint i, n = parser->indent; 184 for (i = 0; i < n; i++) 185 g_string_append_c(str, '>'); 186 g_string_append_c(str, ' '); 187 } 188 } else 189 parser->newline = FALSE; 190 } 191 192 static void sc_html_append_str(SC_HTMLParser *parser, const gchar *str, gint len) 193 { 194 GString *string = parser->str; 195 196 if (!parser->pre && parser->space) { 197 g_string_append_c(string, ' '); 198 parser->space = FALSE; 199 } 200 201 if (len == 0) return; 202 if (len < 0) 203 g_string_append(string, str); 204 else { 205 gchar *s; 206 Xstrndup_a(s, str, len, return); 207 g_string_append(string, s); 208 } 209 210 parser->empty_line = FALSE; 211 if (string->len > 0 && string->str[string->len - 1] == '\n') { 212 parser->newline = TRUE; 213 if (string->len > 1 && string->str[string->len - 2] == '\n') 214 parser->empty_line = TRUE; 215 } else 216 parser->newline = FALSE; 217 } 218 219 static SC_HTMLTag *sc_html_get_tag(const gchar *str) 220 { 221 SC_HTMLTag *tag; 222 gchar *tmp; 223 guchar *tmpp; 224 225 cm_return_val_if_fail(str != NULL, NULL); 226 227 if (*str == '\0' || *str == '!') return NULL; 228 229 Xstrdup_a(tmp, str, return NULL); 230 231 tag = g_new0(SC_HTMLTag, 1); 232 233 for (tmpp = tmp; *tmpp != '\0' && !g_ascii_isspace(*tmpp); tmpp++) 234 ; 235 236 if (*tmpp == '\0') { 237 tag->name = g_utf8_strdown(tmp, -1); 238 return tag; 239 } else { 240 *tmpp++ = '\0'; 241 tag->name = g_utf8_strdown(tmp, -1); 242 } 243 244 while (*tmpp != '\0') { 245 SC_HTMLAttr *attr; 246 gchar *attr_name; 247 gchar *attr_value; 248 gchar *p; 249 gchar quote; 250 251 while (g_ascii_isspace(*tmpp)) tmpp++; 252 attr_name = tmpp; 253 254 while (*tmpp != '\0' && !g_ascii_isspace(*tmpp) && 255 *tmpp != '=') 256 tmpp++; 257 if (*tmpp != '\0' && *tmpp != '=') { 258 *tmpp++ = '\0'; 259 while (g_ascii_isspace(*tmpp)) tmpp++; 260 } 261 262 if (*tmpp == '=') { 263 *tmpp++ = '\0'; 264 while (g_ascii_isspace(*tmpp)) tmpp++; 265 266 if (*tmpp == '"' || *tmpp == '\'') { 267 /* name="value" */ 268 quote = *tmpp; 269 tmpp++; 270 attr_value = tmpp; 271 if ((p = strchr(attr_value, quote)) == NULL) { 272 if (debug_get_mode()) { 273 g_warning("sc_html_get_tag(): syntax error in tag: '%s'", 274 str); 275 } else { 276 gchar *cut = g_strndup(str, 100); 277 g_warning("sc_html_get_tag(): syntax error in tag: '%s%s'", 278 cut, strlen(str)>100?"...":"."); 279 g_free(cut); 280 } 281 return tag; 282 } 283 tmpp = p; 284 *tmpp++ = '\0'; 285 while (g_ascii_isspace(*tmpp)) tmpp++; 286 } else { 287 /* name=value */ 288 attr_value = tmpp; 289 while (*tmpp != '\0' && !g_ascii_isspace(*tmpp)) tmpp++; 290 if (*tmpp != '\0') 291 *tmpp++ = '\0'; 292 } 293 } else 294 attr_value = ""; 295 296 g_strchomp(attr_name); 297 attr = g_new(SC_HTMLAttr, 1); 298 attr->name = g_utf8_strdown(attr_name, -1); 299 attr->value = g_strdup(attr_value); 300 tag->attr = g_list_append(tag->attr, attr); 301 } 302 303 return tag; 304 } 305 306 static void sc_html_free_tag(SC_HTMLTag *tag) 307 { 308 if (!tag) return; 309 310 g_free(tag->name); 311 while (tag->attr != NULL) { 312 SC_HTMLAttr *attr = (SC_HTMLAttr *)tag->attr->data; 313 g_free(attr->name); 314 g_free(attr->value); 315 g_free(attr); 316 tag->attr = g_list_remove(tag->attr, tag->attr->data); 317 } 318 g_free(tag); 319 } 320 321 static void decode_href(SC_HTMLParser *parser) 322 { 323 gchar *tmp; 324 SC_HTMLParser *tparser = g_new0(SC_HTMLParser, 1); 325 326 tparser->str = g_string_new(NULL); 327 tparser->buf = g_string_new(parser->href); 328 tparser->bufp = tparser->buf->str; 329 330 tmp = sc_html_parse(tparser); 331 332 g_free(parser->href); 333 parser->href = g_strdup(tmp); 334 335 sc_html_parser_destroy(tparser); 336 } 337 338 static SC_HTMLState sc_html_parse_tag(SC_HTMLParser *parser) 339 { 340 gchar buf[SC_HTMLBUFSIZE]; 341 SC_HTMLTag *tag; 342 343 sc_html_get_parenthesis(parser, buf, sizeof(buf)); 344 345 tag = sc_html_get_tag(buf); 346 347 parser->state = SC_HTML_UNKNOWN; 348 if (!tag) return SC_HTML_UNKNOWN; 349 350 if (!strcmp(tag->name, "br") || !strcmp(tag->name, "br/")) { 351 parser->space = FALSE; 352 sc_html_append_char(parser, '\n'); 353 parser->state = SC_HTML_BR; 354 } else if (!strcmp(tag->name, "a")) { 355 GList *cur; 356 if (parser->href != NULL) { 357 g_free(parser->href); 358 parser->href = NULL; 359 } 360 for (cur = tag->attr; cur != NULL; cur = cur->next) { 361 if (cur->data && !strcmp(((SC_HTMLAttr *)cur->data)->name, "href")) { 362 g_free(parser->href); 363 parser->href = g_strdup(((SC_HTMLAttr *)cur->data)->value); 364 decode_href(parser); 365 parser->state = SC_HTML_HREF_BEG; 366 break; 367 } 368 } 369 if (parser->href == NULL) 370 parser->href = g_strdup(""); 371 parser->state = SC_HTML_HREF_BEG; 372 } else if (!strcmp(tag->name, "/a")) { 373 parser->state = SC_HTML_HREF; 374 } else if (!strcmp(tag->name, "p")) { 375 parser->space = FALSE; 376 if (!parser->empty_line) { 377 parser->space = FALSE; 378 if (!parser->newline) sc_html_append_char(parser, '\n'); 379 sc_html_append_char(parser, '\n'); 380 } 381 parser->state = SC_HTML_PAR; 382 } else if (!strcmp(tag->name, "pre")) { 383 parser->pre = TRUE; 384 parser->state = SC_HTML_PRE; 385 } else if (!strcmp(tag->name, "/pre")) { 386 parser->pre = FALSE; 387 parser->state = SC_HTML_NORMAL; 388 } else if (!strcmp(tag->name, "hr")) { 389 if (!parser->newline) { 390 parser->space = FALSE; 391 sc_html_append_char(parser, '\n'); 392 } 393 sc_html_append_str(parser, HR_STR, -1); 394 sc_html_append_char(parser, '\n'); 395 parser->state = SC_HTML_HR; 396 } else if (!strcmp(tag->name, "div") || 397 !strcmp(tag->name, "ul") || 398 !strcmp(tag->name, "li") || 399 !strcmp(tag->name, "table") || 400 !strcmp(tag->name, "dd") || 401 !strcmp(tag->name, "tr")) { 402 if (!parser->newline) { 403 parser->space = FALSE; 404 sc_html_append_char(parser, '\n'); 405 } 406 if (!strcmp(tag->name, "li")) { 407 sc_html_append_str(parser, LI_STR, -1); 408 } 409 parser->state = SC_HTML_NORMAL; 410 } else if (tag->name[0] == 'h' && g_ascii_isdigit(tag->name[1])) { 411 if (!parser->newline) { 412 parser->space = FALSE; 413 sc_html_append_char(parser, '\n'); 414 } 415 sc_html_append_char(parser, '\n'); 416 } else if (!strcmp(tag->name, "blockquote")) { 417 parser->state = SC_HTML_NORMAL; 418 parser->indent++; 419 } else if (!strcmp(tag->name, "/blockquote")) { 420 parser->state = SC_HTML_NORMAL; 421 parser->indent--; 422 } else if (!strcmp(tag->name, "/table") || 423 (tag->name[0] == '/' && 424 tag->name[1] == 'h' && 425 g_ascii_isdigit(tag->name[2]))) { 426 if (!parser->empty_line) { 427 parser->space = FALSE; 428 if (!parser->newline) sc_html_append_char(parser, '\n'); 429 sc_html_append_char(parser, '\n'); 430 } 431 parser->state = SC_HTML_NORMAL; 432 } else if (!strcmp(tag->name, "/div") || 433 !strcmp(tag->name, "/ul") || 434 !strcmp(tag->name, "/li")) { 435 if (!parser->newline) { 436 parser->space = FALSE; 437 sc_html_append_char(parser, '\n'); 438 } 439 parser->state = SC_HTML_NORMAL; 440 } 441 442 sc_html_free_tag(tag); 443 444 return parser->state; 445 } 446 447 static void sc_html_parse_special(SC_HTMLParser *parser) 448 { 449 gchar *entity; 450 451 parser->state = SC_HTML_UNKNOWN; 452 cm_return_if_fail(*parser->bufp == '&'); 453 454 entity = entity_decode(parser->bufp); 455 if (entity != NULL) { 456 sc_html_append_str(parser, entity, -1); 457 g_free(entity); 458 while (*parser->bufp++ != ';'); 459 } else { 460 /* output literal `&' */ 461 sc_html_append_char(parser, *parser->bufp++); 462 } 463 parser->state = SC_HTML_NORMAL; 464 } 465 466 static gchar *sc_html_find_tag(SC_HTMLParser *parser, const gchar *tag) 467 { 468 gchar *cur = parser->bufp; 469 gint len = strlen(tag); 470 471 if (cur == NULL) 472 return NULL; 473 474 while ((cur = strstr(cur, "<")) != NULL) { 475 if (!g_ascii_strncasecmp(cur, tag, len)) 476 return cur; 477 cur += 2; 478 } 479 return NULL; 480 } 481 482 static void sc_html_get_parenthesis(SC_HTMLParser *parser, gchar *buf, gint len) 483 { 484 gchar *p; 485 486 buf[0] = '\0'; 487 cm_return_if_fail(*parser->bufp == '<'); 488 489 /* ignore comment / CSS / script stuff */ 490 if (!strncmp(parser->bufp, "<!--", 4)) { 491 parser->bufp += 4; 492 while ((p = strstr(parser->bufp, "-->")) == NULL) 493 if (sc_html_read_line(parser) == SC_HTML_EOF) return; 494 parser->bufp = p + 3; 495 return; 496 } 497 if (!g_ascii_strncasecmp(parser->bufp, "<style", 6)) { 498 parser->bufp += 6; 499 while ((p = sc_html_find_tag(parser, "</style>")) == NULL) 500 if (sc_html_read_line(parser) == SC_HTML_EOF) return; 501 parser->bufp = p + 8; 502 return; 503 } 504 if (!g_ascii_strncasecmp(parser->bufp, "<script", 7)) { 505 parser->bufp += 7; 506 while ((p = sc_html_find_tag(parser, "</script>")) == NULL) 507 if (sc_html_read_line(parser) == SC_HTML_EOF) return; 508 parser->bufp = p + 9; 509 return; 510 } 511 512 parser->bufp++; 513 while ((p = strchr(parser->bufp, '>')) == NULL) 514 if (sc_html_read_line(parser) == SC_HTML_EOF) return; 515 516 strncpy2(buf, parser->bufp, MIN(p - parser->bufp + 1, len)); 517 g_strstrip(buf); 518 parser->bufp = p + 1; 519 }