html_extractor.c (30172B)
1 /* 2 This file is part of libextractor. 3 Copyright (C) 2002, 2003, 2004, 2005, 2009, 2012 Vidyut Samanta and Christian Grothoff 4 5 libextractor is free software; you can redistribute it and/or modify 6 it under the terms of the GNU General Public License as published 7 by the Free Software Foundation; either version 2, or (at your 8 option) any later version. 9 10 libextractor is distributed in the hope that it will be useful, but 11 WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 General Public License for more details. 14 15 You should have received a copy of the GNU General Public License 16 along with libextractor; see the file COPYING. If not, write to the 17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 18 Boston, MA 02110-1301, USA. 19 20 */ 21 /** 22 * @file plugins/html_extractor.c 23 * @brief plugin to support HTML files 24 * @author Christian Grothoff 25 * 26 * With the "fulltext" option the text of the <body> is reported as one 27 * #EXTRACTOR_METATYPE_FULL_TEXT item. The file is then read as a whole 28 * (up to #MAX_FULLTEXT_INPUT bytes), converted to UTF-8 according to 29 * its byte order mark, XML declaration or META charset (sniffing UTF-8 30 * versus windows-1252 without one) and parsed by libtidy from memory. 31 * The walk over the tree drops script, style, noscript, template, the 32 * page margins (nav, aside, header and footer outside of an article or 33 * section, elements with an ARIA role of navigation, banner, 34 * contentinfo, complementary or search) and hidden elements (hidden, 35 * aria-hidden="true", display:none); block-level elements end 36 * paragraphs, as do two consecutive <br>. 37 */ 38 #include "platform.h" 39 #include "extractor.h" 40 #include "fulltext.h" 41 #include <magic.h> 42 #if HAVE_TIDY_H 43 #include <tidy.h> 44 #include <tidybuffio.h> 45 #elif HAVE_TIDY_TIDY_H 46 #include <tidy/tidy.h> 47 #include <tidy/tidybuffio.h> 48 #else 49 Broken build, fix tidy detection. 50 #endif 51 52 /** 53 * Mapping of HTML META names to LE types. 54 */ 55 static struct 56 { 57 /** 58 * HTML META name. 59 */ 60 const char *name; 61 62 /** 63 * Corresponding LE type. 64 */ 65 enum EXTRACTOR_MetaType type; 66 } tagmap[] = { 67 { "author", EXTRACTOR_METATYPE_AUTHOR_NAME }, 68 { "dc.author", EXTRACTOR_METATYPE_AUTHOR_NAME }, 69 { "title", EXTRACTOR_METATYPE_TITLE }, 70 { "dc.title", EXTRACTOR_METATYPE_TITLE}, 71 { "description", EXTRACTOR_METATYPE_DESCRIPTION }, 72 { "dc.description", EXTRACTOR_METATYPE_DESCRIPTION }, 73 { "subject", EXTRACTOR_METATYPE_SUBJECT}, 74 { "dc.subject", EXTRACTOR_METATYPE_SUBJECT}, 75 { "date", EXTRACTOR_METATYPE_UNKNOWN_DATE }, 76 { "dc.date", EXTRACTOR_METATYPE_UNKNOWN_DATE}, 77 { "publisher", EXTRACTOR_METATYPE_PUBLISHER }, 78 { "dc.publisher", EXTRACTOR_METATYPE_PUBLISHER}, 79 { "rights", EXTRACTOR_METATYPE_RIGHTS }, 80 { "dc.rights", EXTRACTOR_METATYPE_RIGHTS }, 81 { "copyright", EXTRACTOR_METATYPE_COPYRIGHT }, 82 { "language", EXTRACTOR_METATYPE_LANGUAGE }, 83 { "keywords", EXTRACTOR_METATYPE_KEYWORDS }, 84 { "abstract", EXTRACTOR_METATYPE_ABSTRACT }, 85 { "formatter", EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE }, 86 { "dc.creator", EXTRACTOR_METATYPE_CREATOR}, 87 { "dc.identifier", EXTRACTOR_METATYPE_URI }, 88 { "dc.format", EXTRACTOR_METATYPE_FORMAT }, 89 { NULL, EXTRACTOR_METATYPE_RESERVED } 90 }; 91 92 93 /** 94 * Global handle to MAGIC data. 95 */ 96 static magic_t magic; 97 98 99 /** 100 * Map 'meta' tag to LE type. 101 * 102 * @param tag tag to map 103 * @return EXTRACTOR_METATYPE_RESERVED if the type was not found 104 */ 105 static enum EXTRACTOR_MetaType 106 tag_to_type (const char *tag) 107 { 108 unsigned int i; 109 110 for (i = 0; NULL != tagmap[i].name; i++) 111 if (0 == strcasecmp (tag, 112 tagmap[i].name)) 113 return tagmap[i].type; 114 return EXTRACTOR_METATYPE_RESERVED; 115 } 116 117 118 /** 119 * Function called by libtidy for error reporting. 120 * 121 * @param doc tidy doc being processed 122 * @param lvl report level 123 * @param line input line 124 * @param col input column 125 * @param mssg message 126 * @return FALSE (no output) 127 */ 128 static Bool TIDY_CALL 129 report_cb (TidyDoc doc, 130 TidyReportLevel lvl, 131 uint line, 132 uint col, 133 ctmbstr mssg) 134 { 135 return 0; 136 } 137 138 139 /** 140 * Input callback: get next byte of input. 141 * 142 * @param sourceData our 'struct EXTRACTOR_ExtractContext' 143 * @return next byte of input, EndOfStream on errors and EOF 144 */ 145 static int TIDY_CALL 146 get_byte_cb (void *sourceData) 147 { 148 struct EXTRACTOR_ExtractContext *ec = sourceData; 149 void *data; 150 151 if (1 != 152 ec->read (ec->cls, 153 &data, 1)) 154 return EndOfStream; 155 return *(unsigned char*) data; 156 } 157 158 159 /** 160 * Input callback: unget last byte of input. 161 * 162 * @param sourceData our 'struct EXTRACTOR_ExtractContext' 163 * @param bt byte to unget (ignored) 164 */ 165 static void TIDY_CALL 166 unget_byte_cb (void *sourceData, byte bt) 167 { 168 struct EXTRACTOR_ExtractContext *ec = sourceData; 169 170 (void) ec->seek (ec->cls, -1, SEEK_CUR); 171 } 172 173 174 /** 175 * Input callback: check for EOF. 176 * 177 * @param sourceData our 'struct EXTRACTOR_ExtractContext' 178 * @return true if we are at the EOF 179 */ 180 static Bool TIDY_CALL 181 eof_cb (void *sourceData) 182 { 183 struct EXTRACTOR_ExtractContext *ec = sourceData; 184 185 return ec->seek (ec->cls, 0, SEEK_CUR) == ec->get_size (ec->cls); 186 } 187 188 189 /** 190 * Report the TITLE and the META tags found in the document head. 191 * 192 * @param ec extraction context 193 * @param doc parsed document 194 * @return 1 if the caller should stop extracting, 0 to continue 195 */ 196 static int 197 report_head (struct EXTRACTOR_ExtractContext *ec, 198 TidyDoc doc) 199 { 200 TidyNode head; 201 TidyNode child; 202 TidyNode title; 203 const char *name; 204 TidyBuffer tbuf; 205 TidyAttr attr; 206 enum EXTRACTOR_MetaType type; 207 208 if (NULL == (head = tidyGetHead (doc))) 209 return 0; 210 for (child = tidyGetChild (head); NULL != child; child = tidyGetNext (child)) 211 { 212 switch (tidyNodeGetType (child)) 213 { 214 case TidyNode_Start: 215 case TidyNode_StartEnd: 216 name = tidyNodeGetName (child); 217 if (NULL == name) 218 break; 219 if ( (0 == strcasecmp (name, "title")) && 220 (NULL != (title = tidyGetChild (child))) ) 221 { 222 tidyBufInit (&tbuf); 223 tidyNodeGetValue (doc, title, &tbuf); 224 /* add 0-termination */ 225 tidyBufPutByte (&tbuf, 0); 226 if (0 != 227 ec->proc (ec->cls, 228 "html", 229 EXTRACTOR_METATYPE_TITLE, 230 EXTRACTOR_METAFORMAT_UTF8, 231 "text/plain", 232 (const char *) tbuf.bp, 233 tbuf.size)) 234 { 235 tidyBufFree (&tbuf); 236 return 1; 237 } 238 tidyBufFree (&tbuf); 239 break; 240 } 241 if (0 == strcasecmp (name, "meta")) 242 { 243 if (NULL == (attr = tidyAttrGetById (child, 244 TidyAttr_NAME))) 245 break; 246 name = tidyAttrValue (attr); 247 if (NULL == name) 248 break; 249 if (EXTRACTOR_METATYPE_RESERVED == 250 (type = tag_to_type (name))) 251 break; 252 if (NULL == (attr = tidyAttrGetById (child, 253 TidyAttr_CONTENT))) 254 break; 255 name = tidyAttrValue (attr); 256 if (NULL == name) 257 break; 258 if (0 != 259 ec->proc (ec->cls, 260 "html", 261 type, 262 EXTRACTOR_METAFORMAT_UTF8, 263 "text/plain", 264 name, 265 strlen (name) + 1)) 266 return 1; 267 break; 268 } 269 break; 270 default: 271 break; 272 } 273 } 274 return 0; 275 } 276 277 278 /* ------------------------------------------------------------------ */ 279 /* Full text */ 280 /* ------------------------------------------------------------------ */ 281 282 /** 283 * Largest HTML file we read for the "fulltext" option. libtidy keeps 284 * the whole tree in memory (several times the size of the input), and 285 * the text we may emit is capped at #EXTRACTOR_FULLTEXT_MAX anyway; 286 * longer files are truncated. 287 */ 288 #define MAX_FULLTEXT_INPUT (16 * 1024 * 1024) 289 290 /** 291 * How deep into the element tree we descend; anything nested deeper 292 * is ignored. Real documents stay far below this, and it bounds the 293 * stack use of #walk_children(). 294 */ 295 #define MAX_DEPTH 256 296 297 /** 298 * How many attributes of one element we look at. 299 */ 300 #define MAX_ATTRS 64 301 302 /** 303 * How far into the file we look for a charset declaration. 304 */ 305 #define CHARSET_SCAN 8192 306 307 308 /** 309 * State of the full text walk over the tidy tree. 310 */ 311 struct TextWalk 312 { 313 /** 314 * Text collected so far. 315 */ 316 struct EXTRACTOR_FullText ft; 317 318 /** 319 * The document. 320 */ 321 TidyDoc doc; 322 323 /** 324 * Scratch buffer for text node values. 325 */ 326 TidyBuffer tb; 327 328 /** 329 * 1 if the last thing we saw was a <br> (with nothing but white 330 * space after it), so another one ends the paragraph. 331 */ 332 int after_br; 333 }; 334 335 336 /** 337 * Find @a needle in @a hay, ignoring ASCII case. 338 * 339 * @param hay buffer to search 340 * @param hlen number of bytes in @a hay 341 * @param needle 0-terminated lower-case string to look for 342 * @return offset of the match, or @a hlen if there is none 343 */ 344 static size_t 345 find_ci (const char *hay, 346 size_t hlen, 347 const char *needle) 348 { 349 size_t nlen = strlen (needle); 350 351 if (nlen > hlen) 352 return hlen; 353 for (size_t i = 0; i + nlen <= hlen; i++) 354 { 355 size_t k; 356 357 for (k = 0; k < nlen; k++) 358 if (tolower ((unsigned char) hay[i + k]) != needle[k]) 359 break; 360 if (k == nlen) 361 return i; 362 } 363 return hlen; 364 } 365 366 367 /** 368 * Parse a charset name following "charset" / "encoding" at @a p: 369 * optional white space, '=', optional white space and quotes, then the 370 * name. 371 * 372 * @param p where to start 373 * @param end end of the buffer 374 * @param[out] cs where to write the name (0-terminated) 375 * @param cs_size number of bytes available in @a cs 376 * @return 1 if a name was found 377 */ 378 static int 379 parse_charset_value (const char *p, 380 const char *end, 381 char *cs, 382 size_t cs_size) 383 { 384 size_t o = 0; 385 386 while ( (p < end) && isspace ((unsigned char) *p)) 387 p++; 388 if ( (p >= end) || ('=' != *p) ) 389 return 0; 390 p++; 391 while ( (p < end) && 392 (isspace ((unsigned char) *p) || ('"' == *p) || ('\'' == *p) ) ) 393 p++; 394 while ( (p < end) && 395 (o + 1 < cs_size) && 396 (isalnum ((unsigned char) *p) || 397 ('-' == *p) || ('_' == *p) || ('.' == *p) || (':' == *p) ) ) 398 cs[o++] = (char) tolower ((unsigned char) *p++); 399 cs[o] = '\0'; 400 return (0 != o); 401 } 402 403 404 /** 405 * Is @a data valid UTF-8? 406 * 407 * @param data bytes to check 408 * @param len number of bytes in @a data 409 * @return 1 if valid 410 */ 411 static int 412 is_utf8 (const unsigned char *data, 413 size_t len) 414 { 415 size_t i = 0; 416 417 while (i < len) 418 { 419 unsigned int n; 420 uint32_t cp; 421 422 if (data[i] < 0x80) 423 { 424 i++; 425 continue; 426 } 427 if ( (data[i] >= 0xC2) && (data[i] <= 0xDF) ) 428 n = 1; 429 else if ( (data[i] >= 0xE0) && (data[i] <= 0xEF) ) 430 n = 2; 431 else if ( (data[i] >= 0xF0) && (data[i] <= 0xF4) ) 432 n = 3; 433 else 434 return 0; 435 if (n > len - i - 1) 436 return 0; 437 cp = data[i] & (0x3F >> n); 438 for (unsigned int k = 1; k <= n; k++) 439 { 440 if (0x80 != (data[i + k] & 0xC0)) 441 return 0; 442 cp = (cp << 6) | (data[i + k] & 0x3F); 443 } 444 if ( ( (2 == n) && (cp < 0x800) ) || 445 ( (3 == n) && ( (cp < 0x10000) || (cp > 0x10FFFF) ) ) || 446 ( (cp >= 0xD800) && (cp <= 0xDFFF) ) ) 447 return 0; 448 i += n + 1; 449 } 450 return 1; 451 } 452 453 454 /** 455 * Determine the character set of an HTML document: a byte order mark, 456 * then an XML declaration, then a META charset / http-equiv 457 * declaration; without any of those, UTF-8 if the bytes are valid 458 * UTF-8 and windows-1252 otherwise. Labels are mapped as browsers do 459 * (ISO-8859-1 and ASCII mean windows-1252; a META tag claiming UTF-16 460 * means UTF-8). 461 * 462 * @param data start of the document 463 * @param len number of bytes in @a data 464 * @param[out] cs where to write the iconv name of the charset 465 * @param cs_size number of bytes available in @a cs 466 * @return number of BOM bytes to skip 467 */ 468 static size_t 469 sniff_charset (const char *data, 470 size_t len, 471 char *cs, 472 size_t cs_size) 473 { 474 const unsigned char *u = (const unsigned char *) data; 475 size_t scan = (len > CHARSET_SCAN) ? CHARSET_SCAN : len; 476 char found[64]; 477 int have = 0; 478 479 if ( (len >= 3) && (0xEF == u[0]) && (0xBB == u[1]) && (0xBF == u[2]) ) 480 { 481 snprintf (cs, cs_size, "UTF-8"); 482 return 3; 483 } 484 if ( (len >= 2) && (0xFE == u[0]) && (0xFF == u[1]) ) 485 { 486 snprintf (cs, cs_size, "UTF-16BE"); 487 return 2; 488 } 489 if ( (len >= 2) && (0xFF == u[0]) && (0xFE == u[1]) ) 490 { 491 snprintf (cs, cs_size, "UTF-16LE"); 492 return 2; 493 } 494 if ( (scan >= 5) && (0 == memcmp (data, "<?xml", 5)) ) 495 { 496 const char *gt = memchr (data, '>', scan); 497 size_t dlen = (NULL == gt) ? scan : (size_t) (gt - data); 498 size_t off = find_ci (data, dlen, "encoding"); 499 500 if (off < dlen) 501 have = parse_charset_value (&data[off + strlen ("encoding")], 502 &data[dlen], 503 found, 504 sizeof (found)); 505 } 506 for (size_t pos = 0; (! have) && (pos < scan); ) 507 { 508 size_t off = find_ci (&data[pos], scan - pos, "<meta"); 509 const char *tag; 510 const char *gt; 511 size_t tlen; 512 size_t c; 513 514 if (off >= scan - pos) 515 break; 516 tag = &data[pos + off]; 517 gt = memchr (tag, '>', scan - pos - off); 518 tlen = (NULL == gt) ? scan - pos - off : (size_t) (gt - tag); 519 c = find_ci (tag, tlen, "charset"); 520 if (c < tlen) 521 have = parse_charset_value (&tag[c + strlen ("charset")], 522 &tag[tlen], 523 found, 524 sizeof (found)); 525 pos += off + 5; 526 } 527 if (have) 528 { 529 if ( (0 == strcmp (found, "iso-8859-1")) || 530 (0 == strcmp (found, "iso8859-1")) || 531 (0 == strcmp (found, "latin1")) || 532 (0 == strcmp (found, "l1")) || 533 (0 == strcmp (found, "us-ascii")) || 534 (0 == strcmp (found, "ascii")) || 535 (0 == strcmp (found, "cp1252")) || 536 (0 == strcmp (found, "windows-1252")) ) 537 snprintf (cs, cs_size, "WINDOWS-1252"); 538 else if ( (0 == strncmp (found, "utf-16", 6)) || 539 (0 == strncmp (found, "utf16", 5)) || 540 (0 == strcmp (found, "utf8")) || 541 (0 == strcmp (found, "utf-8")) ) 542 snprintf (cs, cs_size, "UTF-8"); 543 else 544 snprintf (cs, cs_size, "%s", found); 545 return 0; 546 } 547 if (is_utf8 (u, len)) 548 snprintf (cs, cs_size, "UTF-8"); 549 else 550 snprintf (cs, cs_size, "WINDOWS-1252"); 551 return 0; 552 } 553 554 555 /** 556 * Code points of windows-1252 bytes 0x80 to 0x9F. 557 */ 558 static const uint16_t cp1252[32] = { 559 0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 560 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x017D, 0x008F, 561 0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 562 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x009D, 0x017E, 0x0178 563 }; 564 565 566 /** 567 * Append the UTF-8 encoding of @a cp (which must be below 0x10000) 568 * to @a out. 569 * 570 * @param out output buffer, at least 3 bytes must be available 571 * @param cp code point 572 * @return number of bytes written 573 */ 574 static size_t 575 put_utf8 (char *out, 576 uint32_t cp) 577 { 578 if (cp < 0x80) 579 { 580 out[0] = (char) cp; 581 return 1; 582 } 583 if (cp < 0x800) 584 { 585 out[0] = (char) (0xC0 | (cp >> 6)); 586 out[1] = (char) (0x80 | (cp & 0x3F)); 587 return 2; 588 } 589 out[0] = (char) (0xE0 | (cp >> 12)); 590 out[1] = (char) (0x80 | ((cp >> 6) & 0x3F)); 591 out[2] = (char) (0x80 | (cp & 0x3F)); 592 return 3; 593 } 594 595 596 /** 597 * Convert @a data from charset @a cs to UTF-8. Undecodable bytes 598 * become U+FFFD. If the charset is unknown, the bytes are taken as 599 * UTF-8 if they are valid UTF-8, and as windows-1252 otherwise. 600 * 601 * @param data input 602 * @param len number of bytes in @a data 603 * @param cs charset name as understood by iconv 604 * @param[out] olen number of bytes in the result 605 * @return converted text, NULL on allocation failure; caller frees 606 */ 607 static char * 608 to_utf8 (const char *data, 609 size_t len, 610 const char *cs, 611 size_t *olen) 612 { 613 char *out; 614 size_t o = 0; 615 616 *olen = 0; 617 if (0 == strcmp (cs, "UTF-8")) 618 { 619 if (NULL == (out = malloc (len + 1))) 620 return NULL; 621 memcpy (out, data, len); 622 *olen = len; 623 return out; 624 } 625 /* no conversion produces more than 3 bytes of UTF-8 per input byte 626 (a UTF-16 surrogate pair is 4 bytes in and 4 bytes out) */ 627 if (NULL == (out = malloc (3 * len + 4))) 628 return NULL; 629 #if HAVE_ICONV 630 if (0 != strcmp (cs, "WINDOWS-1252")) 631 { 632 iconv_t cd = iconv_open ("UTF-8", cs); 633 634 if ((iconv_t) -1 != cd) 635 { 636 char *ib = (char *) data; 637 size_t il = len; 638 char *ob = out; 639 size_t ol = 3 * len; 640 641 while (il > 0) 642 { 643 if ((size_t) -1 != 644 iconv (cd, &ib, &il, &ob, &ol)) 645 break; 646 if ( (E2BIG == errno) || 647 (ol < 3) ) 648 break; 649 /* skip one undecodable byte */ 650 ob += put_utf8 (ob, 0xFFFD); 651 ol -= 3; 652 ib++; 653 il--; 654 } 655 iconv_close (cd); 656 *olen = (size_t) (ob - out); 657 return out; 658 } 659 if (is_utf8 ((const unsigned char *) data, len)) 660 { 661 memcpy (out, data, len); 662 *olen = len; 663 return out; 664 } 665 } 666 #else 667 if ( (0 != strcmp (cs, "WINDOWS-1252")) && 668 (is_utf8 ((const unsigned char *) data, len)) ) 669 { 670 memcpy (out, data, len); 671 *olen = len; 672 return out; 673 } 674 #endif 675 for (size_t i = 0; i < len; i++) 676 { 677 unsigned char c = (unsigned char) data[i]; 678 679 if (c < 0x80) 680 out[o++] = (char) c; 681 else if (c < 0xA0) 682 o += put_utf8 (&out[o], cp1252[c - 0x80]); 683 else 684 o += put_utf8 (&out[o], c); 685 } 686 *olen = o; 687 return out; 688 } 689 690 691 /** 692 * Read the whole input (up to #MAX_FULLTEXT_INPUT bytes). 693 * 694 * @param ec extraction context, positioned at the start 695 * @param[out] len number of bytes read 696 * @return the data, NULL on error; caller frees 697 */ 698 static char * 699 read_all (struct EXTRACTOR_ExtractContext *ec, 700 size_t *len) 701 { 702 char *buf = NULL; 703 size_t size = 0; 704 size_t cap = 0; 705 706 *len = 0; 707 while (size < MAX_FULLTEXT_INPUT) 708 { 709 void *data; 710 ssize_t got; 711 size_t want = MAX_FULLTEXT_INPUT - size; 712 713 if (want > 64 * 1024) 714 want = 64 * 1024; 715 got = ec->read (ec->cls, &data, want); 716 if (got <= 0) 717 break; 718 if ((size_t) got > want) 719 got = (ssize_t) want; 720 if (size + (size_t) got > cap) 721 { 722 size_t ncap = (0 == cap) ? 64 * 1024 : cap; 723 char *nbuf; 724 725 while (ncap < size + (size_t) got) 726 ncap *= 2; 727 if (ncap > MAX_FULLTEXT_INPUT) 728 ncap = MAX_FULLTEXT_INPUT; 729 if (NULL == (nbuf = realloc (buf, ncap))) 730 break; 731 buf = nbuf; 732 cap = ncap; 733 } 734 memcpy (&buf[size], data, (size_t) got); 735 size += (size_t) got; 736 } 737 *len = size; 738 return buf; 739 } 740 741 742 /** 743 * Does the space-separated token list @a list contain @a tok (ASCII 744 * case-insensitively)? 745 * 746 * @param list token list 747 * @param tok token to look for 748 * @return 1 if found 749 */ 750 static int 751 has_token (const char *list, 752 const char *tok) 753 { 754 size_t tlen = strlen (tok); 755 const char *p = list; 756 757 while ('\0' != *p) 758 { 759 size_t n; 760 761 while (isspace ((unsigned char) *p)) 762 p++; 763 n = 0; 764 while ( ('\0' != p[n]) && (! isspace ((unsigned char) p[n])) ) 765 n++; 766 if ( (n == tlen) && 767 (0 == strncasecmp (p, tok, n)) ) 768 return 1; 769 p += n; 770 } 771 return 0; 772 } 773 774 775 /** 776 * Does @a style (a CSS declaration list) hide the element, i.e. set 777 * display:none or visibility:hidden? 778 * 779 * @param style value of the style attribute 780 * @return 1 if the element is hidden 781 */ 782 static int 783 style_hides (const char *style) 784 { 785 char norm[256]; 786 size_t o = 0; 787 788 for (size_t i = 0; ('\0' != style[i]) && (o + 1 < sizeof (norm)); i++) 789 if (! isspace ((unsigned char) style[i])) 790 norm[o++] = (char) tolower ((unsigned char) style[i]); 791 norm[o] = '\0'; 792 return ( (NULL != strstr (norm, "display:none")) || 793 (NULL != strstr (norm, "visibility:hidden")) ); 794 } 795 796 797 /** 798 * Should the element @a node (and everything in it) be left out of the 799 * full text because of its attributes: hidden, aria-hidden="true", an 800 * inline style that hides it, or an ARIA landmark role for the page 801 * margins (navigation, banner, contentinfo, complementary, search) or a 802 * page break marker? 803 * 804 * @param node element to check 805 * @return 1 to skip the element 806 */ 807 static int 808 attrs_skip (TidyNode node) 809 { 810 static const char *roles[] = { 811 "navigation", "banner", "contentinfo", "complementary", "search", 812 "doc-pagebreak", NULL 813 }; 814 unsigned int n = 0; 815 816 for (TidyAttr a = tidyAttrFirst (node); 817 (NULL != a) && (n < MAX_ATTRS); 818 a = tidyAttrNext (a), n++) 819 { 820 const char *name = tidyAttrName (a); 821 const char *val = tidyAttrValue (a); 822 823 if (NULL == name) 824 continue; 825 if (0 == strcasecmp (name, "hidden")) 826 { 827 /* hidden="until-found" is collapsed content that find-in-page 828 reveals, i.e. part of the document text */ 829 if ( (NULL == val) || 830 (0 != strcasecmp (val, "until-found")) ) 831 return 1; 832 continue; 833 } 834 if (NULL == val) 835 continue; 836 if ( (0 == strcasecmp (name, "aria-hidden")) && 837 (0 == strcasecmp (val, "true")) ) 838 return 1; 839 if ( (0 == strcasecmp (name, "style")) && 840 (style_hides (val)) ) 841 return 1; 842 if (0 == strcasecmp (name, "role")) 843 for (unsigned int i = 0; NULL != roles[i]; i++) 844 if (has_token (val, roles[i])) 845 return 1; 846 } 847 return 0; 848 } 849 850 851 /** 852 * Is @a id an element whose content is never document text? 853 * 854 * @param id element to check 855 * @return 1 to skip the element 856 */ 857 static int 858 tag_skip (TidyTagId id) 859 { 860 switch (id) 861 { 862 case TidyTag_HEAD: 863 case TidyTag_TITLE: 864 case TidyTag_SCRIPT: 865 case TidyTag_STYLE: 866 case TidyTag_NOSCRIPT: 867 case TidyTag_TEMPLATE: 868 case TidyTag_NAV: 869 case TidyTag_ASIDE: 870 case TidyTag_SELECT: 871 case TidyTag_DATALIST: 872 case TidyTag_IFRAME: 873 case TidyTag_OBJECT: 874 case TidyTag_APPLET: 875 case TidyTag_RP: 876 case TidyTag_RT: 877 return 1; 878 default: 879 return 0; 880 } 881 } 882 883 884 /** 885 * Does @a id end a paragraph (i.e. is it a block-level element)? 886 * 887 * @param id element to check 888 * @return 1 for block-level elements 889 */ 890 static int 891 tag_is_block (TidyTagId id) 892 { 893 switch (id) 894 { 895 case TidyTag_ADDRESS: 896 case TidyTag_ARTICLE: 897 case TidyTag_BLOCKQUOTE: 898 case TidyTag_BODY: 899 case TidyTag_CAPTION: 900 case TidyTag_CENTER: 901 case TidyTag_DD: 902 case TidyTag_DETAILS: 903 case TidyTag_DIALOG: 904 case TidyTag_DIR: 905 case TidyTag_DIV: 906 case TidyTag_DL: 907 case TidyTag_DT: 908 case TidyTag_FIELDSET: 909 case TidyTag_FIGCAPTION: 910 case TidyTag_FIGURE: 911 case TidyTag_FOOTER: 912 case TidyTag_FORM: 913 case TidyTag_FRAMESET: 914 case TidyTag_H1: 915 case TidyTag_H2: 916 case TidyTag_H3: 917 case TidyTag_H4: 918 case TidyTag_H5: 919 case TidyTag_H6: 920 case TidyTag_HEADER: 921 case TidyTag_HGROUP: 922 case TidyTag_HR: 923 case TidyTag_HTML: 924 case TidyTag_LEGEND: 925 case TidyTag_LI: 926 case TidyTag_LISTING: 927 case TidyTag_MAIN: 928 case TidyTag_MENU: 929 case TidyTag_NOFRAMES: 930 case TidyTag_OL: 931 case TidyTag_P: 932 case TidyTag_PLAINTEXT: 933 case TidyTag_PRE: 934 case TidyTag_SECTION: 935 case TidyTag_SUMMARY: 936 case TidyTag_TABLE: 937 case TidyTag_TBODY: 938 case TidyTag_TD: 939 case TidyTag_TFOOT: 940 case TidyTag_TH: 941 case TidyTag_THEAD: 942 case TidyTag_TR: 943 case TidyTag_UL: 944 case TidyTag_XMP: 945 return 1; 946 default: 947 return 0; 948 } 949 } 950 951 952 /** 953 * Does @a id start a sectioning context, inside which <header> and 954 * <footer> belong to that section rather than to the page? 955 * 956 * @param id element to check 957 * @return 1 for article, aside, main, nav and section 958 */ 959 static int 960 tag_is_sectioning (TidyTagId id) 961 { 962 return (TidyTag_ARTICLE == id) || 963 (TidyTag_ASIDE == id) || 964 (TidyTag_MAIN == id) || 965 (TidyTag_NAV == id) || 966 (TidyTag_SECTION == id); 967 } 968 969 970 /** 971 * Append the text of a text node. 972 * 973 * @param w walk state 974 * @param node the text node 975 */ 976 static void 977 add_text (struct TextWalk *w, 978 TidyNode node) 979 { 980 tidyBufClear (&w->tb); 981 if (! tidyNodeGetValue (w->doc, node, &w->tb)) 982 return; 983 if (0 == w->tb.size) 984 return; 985 for (uint i = 0; i < w->tb.size; i++) 986 if (! isspace (w->tb.bp[i])) 987 { 988 w->after_br = 0; 989 break; 990 } 991 EXTRACTOR_fulltext_append_ (&w->ft, 992 (const char *) w->tb.bp, 993 w->tb.size); 994 } 995 996 997 /** 998 * Append the text of the children of @a node. 999 * 1000 * @param w walk state 1001 * @param node parent node 1002 * @param depth nesting depth of @a node 1003 * @param in_section 1 if @a node is inside a sectioning element 1004 */ 1005 static void 1006 walk_children (struct TextWalk *w, 1007 TidyNode node, 1008 unsigned int depth, 1009 int in_section) 1010 { 1011 if (depth >= MAX_DEPTH) 1012 return; 1013 for (TidyNode c = tidyGetChild (node); 1014 (NULL != c) && (! w->ft.full); 1015 c = tidyGetNext (c)) 1016 { 1017 TidyTagId id; 1018 int block; 1019 1020 switch (tidyNodeGetType (c)) 1021 { 1022 case TidyNode_Text: 1023 case TidyNode_CDATA: 1024 add_text (w, c); 1025 break; 1026 case TidyNode_Start: 1027 case TidyNode_StartEnd: 1028 id = tidyNodeGetId (c); 1029 if (tag_skip (id)) 1030 break; 1031 /* a page-level header or footer is page furniture; one inside 1032 an article or section is that section's heading or byline */ 1033 if ( ( (TidyTag_HEADER == id) || (TidyTag_FOOTER == id) ) && 1034 (! in_section) ) 1035 break; 1036 if (attrs_skip (c)) 1037 break; 1038 if (TidyTag_BR == id) 1039 { 1040 if (w->after_br) 1041 { 1042 EXTRACTOR_fulltext_paragraph_ (&w->ft); 1043 w->after_br = 0; 1044 } 1045 else 1046 { 1047 EXTRACTOR_fulltext_space_ (&w->ft); 1048 w->after_br = 1; 1049 } 1050 break; 1051 } 1052 block = tag_is_block (id); 1053 if (block) 1054 { 1055 EXTRACTOR_fulltext_paragraph_ (&w->ft); 1056 w->after_br = 0; 1057 } 1058 walk_children (w, 1059 c, 1060 depth + 1, 1061 in_section || tag_is_sectioning (id)); 1062 if (block) 1063 { 1064 EXTRACTOR_fulltext_paragraph_ (&w->ft); 1065 w->after_br = 0; 1066 } 1067 break; 1068 default: 1069 break; 1070 } 1071 } 1072 } 1073 1074 1075 /** 1076 * Extract meta data and the full text. Unlike the default mode, this 1077 * reads the whole file, converts it to UTF-8 according to its declared 1078 * (or sniffed) charset and hands tidy a UTF-8 buffer. 1079 * 1080 * @param ec extraction context, positioned at the start of the file 1081 */ 1082 static void 1083 extract_fulltext (struct EXTRACTOR_ExtractContext *ec) 1084 { 1085 struct TextWalk w; 1086 TidyBuffer in; 1087 TidyNode body; 1088 char cs[64]; 1089 char *raw; 1090 char *utf8; 1091 size_t raw_len; 1092 size_t utf8_len; 1093 size_t skip; 1094 1095 if (NULL == (raw = read_all (ec, &raw_len))) 1096 return; 1097 skip = sniff_charset (raw, raw_len, cs, sizeof (cs)); 1098 utf8 = to_utf8 (&raw[skip], raw_len - skip, cs, &utf8_len); 1099 free (raw); 1100 if (NULL == utf8) 1101 return; 1102 if (0 == utf8_len) 1103 { 1104 free (utf8); 1105 return; 1106 } 1107 memset (&w, 0, sizeof (w)); 1108 EXTRACTOR_fulltext_init_ (&w.ft); 1109 EXTRACTOR_fulltext_set_context_ (&w.ft, ec); 1110 if (NULL == (w.doc = tidyCreate ())) 1111 { 1112 free (utf8); 1113 return; 1114 } 1115 tidySetReportFilter (w.doc, &report_cb); 1116 tidySetInCharEncoding (w.doc, "utf8"); 1117 tidyBufInit (&in); 1118 tidyBufAttach (&in, (byte *) utf8, (uint) utf8_len); 1119 tidyBufInit (&w.tb); 1120 if (0 > tidyParseBuffer (w.doc, &in)) 1121 goto CLEANUP; 1122 /* the default mode insists on status 1 (warnings), which a document 1123 that no longer has an encoding problem may not produce; errors (2) 1124 still suppress the head items as they do there */ 1125 if ( (2 > tidyStatus (w.doc)) && 1126 (0 != report_head (ec, w.doc)) ) 1127 goto CLEANUP; 1128 if (NULL != (body = tidyGetBody (w.doc))) 1129 walk_children (&w, body, 0, 0); 1130 (void) EXTRACTOR_fulltext_emit_ (&w.ft, ec, "html"); 1131 CLEANUP: 1132 EXTRACTOR_fulltext_clear_ (&w.ft); 1133 tidyBufFree (&w.tb); 1134 tidyBufDetach (&in); 1135 tidyRelease (w.doc); 1136 free (utf8); 1137 } 1138 1139 1140 /** 1141 * Main entry method for the 'text/html' extraction plugin. 1142 * 1143 * @param ec extraction context provided to the plugin 1144 */ 1145 void 1146 EXTRACTOR_html_extract_method (struct EXTRACTOR_ExtractContext *ec); 1147 1148 void 1149 EXTRACTOR_html_extract_method (struct EXTRACTOR_ExtractContext *ec) 1150 { 1151 TidyDoc doc; 1152 TidyInputSource src; 1153 ssize_t iret; 1154 void *data; 1155 const char *mime; 1156 1157 if (-1 == (iret = ec->read (ec->cls, 1158 &data, 1159 16 * 1024))) 1160 return; 1161 if (NULL == (mime = magic_buffer (magic, data, iret))) 1162 return; 1163 if (0 != strncmp (mime, 1164 "text/html", 1165 strlen ("text/html"))) 1166 return; /* not HTML */ 1167 1168 if (0 != ec->seek (ec->cls, 0, SEEK_SET)) 1169 return; /* seek failed !? */ 1170 1171 if (EXTRACTOR_fulltext_requested_ (ec->config)) 1172 { 1173 extract_fulltext (ec); 1174 return; 1175 } 1176 tidyInitSource (&src, ec, 1177 &get_byte_cb, 1178 &unget_byte_cb, 1179 &eof_cb); 1180 if (NULL == (doc = tidyCreate ())) 1181 return; 1182 tidySetReportFilter (doc, &report_cb); 1183 tidySetAppData (doc, ec); 1184 if (0 > tidyParseSource (doc, &src)) 1185 { 1186 tidyRelease (doc); 1187 return; 1188 } 1189 if (1 != tidyStatus (doc)) 1190 { 1191 tidyRelease (doc); 1192 return; 1193 } 1194 (void) report_head (ec, doc); 1195 tidyRelease (doc); 1196 } 1197 1198 1199 /** 1200 * Initialize glib and load magic file. 1201 */ 1202 void __attribute__ ((constructor)) 1203 html_gobject_init (void); 1204 1205 void __attribute__ ((constructor)) 1206 html_gobject_init () 1207 { 1208 magic = magic_open (MAGIC_MIME_TYPE); 1209 if (0 != magic_load (magic, NULL)) 1210 { 1211 /* FIXME: how to deal with errors? */ 1212 } 1213 } 1214 1215 1216 /** 1217 * Destructor for the library, cleans up. 1218 */ 1219 void __attribute__ ((destructor)) 1220 html_ltdl_fini (void); 1221 1222 void __attribute__ ((destructor)) 1223 html_ltdl_fini () 1224 { 1225 if (NULL != magic) 1226 { 1227 magic_close (magic); 1228 magic = NULL; 1229 } 1230 } 1231 1232 1233 /* end of html_extractor.c */