libextractor

GNU libextractor
Log | Files | Refs | Submodules | README | LICENSE

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 */