]> git.donarmstrong.com Git - roundcube.git/blob - program/lib/html2text.php
Imported Upstream version 0.2~stable
[roundcube.git] / program / lib / html2text.php
1 <?php
2
3 /*************************************************************************
4  *                                                                       *
5  * class.html2text.inc                                                   *
6  *                                                                       *
7  *************************************************************************
8  *                                                                       *
9  * Converts HTML to formatted plain text                                 *
10  *                                                                       *
11  * Copyright (c) 2005-2007 Jon Abernathy <jon@chuggnutt.com>             *
12  * All rights reserved.                                                  *
13  *                                                                       *
14  * This script is free software; you can redistribute it and/or modify   *
15  * it under the terms of the GNU General Public License as published by  *
16  * the Free Software Foundation; either version 2 of the License, or     *
17  * (at your option) any later version.                                   *
18  *                                                                       *
19  * The GNU General Public License can be found at                        *
20  * http://www.gnu.org/copyleft/gpl.html.                                 *
21  *                                                                       *
22  * This script is distributed in the hope that it will be useful,        *
23  * but WITHOUT ANY WARRANTY; without even the implied warranty of        *
24  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the          *
25  * GNU General Public License for more details.                          *
26  *                                                                       *
27  * Author(s): Jon Abernathy <jon@chuggnutt.com>                          *
28  *                                                                       *
29  * Last modified: 08/08/07                                               *
30  *                                                                       *
31  *************************************************************************/
32
33
34 /**
35  *  Takes HTML and converts it to formatted, plain text.
36  *
37  *  Thanks to Alexander Krug (http://www.krugar.de/) to pointing out and
38  *  correcting an error in the regexp search array. Fixed 7/30/03.
39  *
40  *  Updated set_html() function's file reading mechanism, 9/25/03.
41  *
42  *  Thanks to Joss Sanglier (http://www.dancingbear.co.uk/) for adding
43  *  several more HTML entity codes to the $search and $replace arrays.
44  *  Updated 11/7/03.
45  *
46  *  Thanks to Darius Kasperavicius (http://www.dar.dar.lt/) for
47  *  suggesting the addition of $allowed_tags and its supporting function
48  *  (which I slightly modified). Updated 3/12/04.
49  *
50  *  Thanks to Justin Dearing for pointing out that a replacement for the
51  *  <TH> tag was missing, and suggesting an appropriate fix.
52  *  Updated 8/25/04.
53  *
54  *  Thanks to Mathieu Collas (http://www.myefarm.com/) for finding a
55  *  display/formatting bug in the _build_link_list() function: email
56  *  readers would show the left bracket and number ("[1") as part of the
57  *  rendered email address.
58  *  Updated 12/16/04.
59  *
60  *  Thanks to Wojciech Bajon (http://histeria.pl/) for submitting code
61  *  to handle relative links, which I hadn't considered. I modified his
62  *  code a bit to handle normal HTTP links and MAILTO links. Also for
63  *  suggesting three additional HTML entity codes to search for.
64  *  Updated 03/02/05.
65  *
66  *  Thanks to Jacob Chandler for pointing out another link condition
67  *  for the _build_link_list() function: "https".
68  *  Updated 04/06/05.
69  *
70  *  Thanks to Marc Bertrand (http://www.dresdensky.com/) for
71  *  suggesting a revision to the word wrapping functionality; if you
72  *  specify a $width of 0 or less, word wrapping will be ignored.
73  *  Updated 11/02/06.
74  *
75  *  *** Big housecleaning updates below:
76  *
77  *  Thanks to Colin Brown (http://www.sparkdriver.co.uk/) for
78  *  suggesting the fix to handle </li> and blank lines (whitespace).
79  *  Christian Basedau (http://www.movetheweb.de/) also suggested the
80  *  blank lines fix.
81  *
82  *  Special thanks to Marcus Bointon (http://www.synchromedia.co.uk/),
83  *  Christian Basedau, Norbert Laposa (http://ln5.co.uk/),
84  *  Bas van de Weijer, and Marijn van Butselaar
85  *  for pointing out my glaring error in the <th> handling. Marcus also
86  *  supplied a host of fixes.
87  *
88  *  Thanks to Jeffrey Silverman (http://www.newtnotes.com/) for pointing
89  *  out that extra spaces should be compressed--a problem addressed with
90  *  Marcus Bointon's fixes but that I had not yet incorporated.
91  *
92  *      Thanks to Daniel Schledermann (http://www.typoconsult.dk/) for
93  *  suggesting a valuable fix with <a> tag handling.
94  *
95  *  Thanks to Wojciech Bajon (again!) for suggesting fixes and additions,
96  *  including the <a> tag handling that Daniel Schledermann pointed
97  *  out but that I had not yet incorporated. I haven't (yet)
98  *  incorporated all of Wojciech's changes, though I may at some
99  *  future time.
100  *
101  *  *** End of the housecleaning updates. Updated 08/08/07.
102  *
103  *  @author Jon Abernathy <jon@chuggnutt.com>
104  *  @version 1.0.0
105  *  @since PHP 4.0.2
106  */
107 class html2text
108 {
109
110     /**
111      *  Contains the HTML content to convert.
112      *
113      *  @var string $html
114      *  @access public
115      */
116     var $html;
117
118     /**
119      *  Contains the converted, formatted text.
120      *
121      *  @var string $text
122      *  @access public
123      */
124     var $text;
125
126     /**
127      *  Maximum width of the formatted text, in columns.
128      *
129      *  Set this value to 0 (or less) to ignore word wrapping
130      *  and not constrain text to a fixed-width column.
131      *
132      *  @var integer $width
133      *  @access public
134      */
135     var $width = 70;
136
137     /**
138      *  List of preg* regular expression patterns to search for,
139      *  used in conjunction with $replace.
140      *
141      *  @var array $search
142      *  @access public
143      *  @see $replace
144      */
145     var $search = array(
146         "/\r/",                                  // Non-legal carriage return
147         "/[\n\t]+/",                             // Newlines and tabs
148         '/[ ]{2,}/',                             // Runs of spaces, pre-handling
149         '/<script[^>]*>.*?<\/script>/i',         // <script>s -- which strip_tags supposedly has problems with
150         '/<style[^>]*>.*?<\/style>/i',           // <style>s -- which strip_tags supposedly has problems with
151         //'/<!-- .* -->/',                         // Comments -- which strip_tags might have problem a with
152         '/<p[^>]*>/i',                           // <P>
153         '/<br[^>]*>/i',                          // <br>
154         '/<i[^>]*>(.*?)<\/i>/i',                 // <i>
155         '/<em[^>]*>(.*?)<\/em>/i',               // <em>
156         '/(<ul[^>]*>|<\/ul>)/i',                 // <ul> and </ul>
157         '/(<ol[^>]*>|<\/ol>)/i',                 // <ol> and </ol>
158         '/<li[^>]*>(.*?)<\/li>/i',               // <li> and </li>
159         '/<li[^>]*>/i',                          // <li>
160         '/<hr[^>]*>/i',                          // <hr>
161         '/(<table[^>]*>|<\/table>)/i',           // <table> and </table>
162         '/(<tr[^>]*>|<\/tr>)/i',                 // <tr> and </tr>
163         '/<td[^>]*>(.*?)<\/td>/i',               // <td> and </td>
164         '/&(nbsp|#160);/i',                      // Non-breaking space
165         '/&(quot|rdquo|ldquo|#8220|#8221|#147|#148);/i',
166                                                          // Double quotes
167         '/&(apos|rsquo|lsquo|#8216|#8217);/i',   // Single quotes
168         '/&gt;/i',                               // Greater-than
169         '/&lt;/i',                               // Less-than
170         '/&(amp|#38);/i',                        // Ampersand
171         '/&(copy|#169);/i',                      // Copyright
172         '/&(trade|#8482|#153);/i',               // Trademark
173         '/&(reg|#174);/i',                       // Registered
174         '/&(mdash|#151|#8212);/i',               // mdash
175         '/&(ndash|minus|#8211|#8722);/i',        // ndash
176         '/&(bull|#149|#8226);/i',                // Bullet
177         '/&(pound|#163);/i',                     // Pound sign
178         '/&(euro|#8364);/i',                     // Euro sign
179         '/&[^&;]+;/i',                           // Unknown/unhandled entities
180         '/[ ]{2,}/'                              // Runs of spaces, post-handling
181     );
182
183     /**
184      *  List of pattern replacements corresponding to patterns searched.
185      *
186      *  @var array $replace
187      *  @access public
188      *  @see $search
189      */
190     var $replace = array(
191         '',                                     // Non-legal carriage return
192         ' ',                                    // Newlines and tabs
193         ' ',                                    // Runs of spaces, pre-handling
194         '',                                     // <script>s -- which strip_tags supposedly has problems with
195         '',                                     // <style>s -- which strip_tags supposedly has problems with
196         //'',                                     // Comments -- which strip_tags might have problem a with
197         "\n\n",                               // <P>
198         "\n",                                   // <br>
199         '_\\1_',                                // <i>
200         '_\\1_',                                // <em>
201         "\n\n",                                 // <ul> and </ul>
202         "\n\n",                                 // <ol> and </ol>
203         "\t* \\1\n",                            // <li> and </li>
204         "\n\t* ",                               // <li>
205         "\n-------------------------\n",        // <hr>
206         "\n\n",                                 // <table> and </table>
207         "\n",                                   // <tr> and </tr>
208         "\t\t\\1\n",                            // <td> and </td>
209         ' ',                                    // Non-breaking space
210         '"',                                    // Double quotes
211         "'",                                    // Single quotes
212         '>',
213         '<',
214         '&',
215         '(c)',
216         '(tm)',
217         '(R)',
218         '--',
219         '-',
220         '*',
221         '£',
222         'EUR',                                  // Euro sign. \80 ?
223         '',                                     // Unknown/unhandled entities
224         ' '                                     // Runs of spaces, post-handling
225     );
226
227     /**
228      *  List of preg* regular expression patterns to search for
229      *  and replace using callback function.
230      *
231      *  @var array $callback_search
232      *  @access public
233      */
234     var $callback_search = array(
235         '/<(h)[123456][^>]*>(.*?)<\/h[123456]>/i', // H1 - H3
236         '/<(b)[^>]*>(.*?)<\/b>/i',                 // <b>
237         '/<(strong)[^>]*>(.*?)<\/strong>/i',       // <strong>
238         '/<(a) [^>]*href=("|\')([^"\']+)\2[^>]*>(.*?)<\/a>/i',
239                                                    // <a href="">
240         '/<(th)[^>]*>(.*?)<\/th>/i',               // <th> and </th>
241     );
242
243    /**
244     *  List of preg* regular expression patterns to search for in PRE body,
245     *  used in conjunction with $pre_replace.
246     *
247     *  @var array $pre_search
248     *  @access public
249     *  @see $pre_replace
250     */
251     var $pre_search = array(
252         "/\n/",
253         "/\t/",
254         '/ /',
255         '/<pre[^>]*>/',
256         '/<\/pre>/'
257     );
258
259     /**
260      *  List of pattern replacements corresponding to patterns searched for PRE body.
261      *
262      *  @var array $pre_replace
263      *  @access public
264      *  @see $pre_search
265      */
266     var $pre_replace = array(
267         '<br>',
268         '&nbsp;&nbsp;&nbsp;&nbsp;',
269         '&nbsp;',
270         '',
271         ''
272     );
273
274     /**
275      *  Contains a list of HTML tags to allow in the resulting text.
276      *
277      *  @var string $allowed_tags
278      *  @access public
279      *  @see set_allowed_tags()
280      */
281     var $allowed_tags = '';
282
283     /**
284      *  Contains the base URL that relative links should resolve to.
285      *
286      *  @var string $url
287      *  @access public
288      */
289     var $url;
290
291     /**
292      *  Indicates whether content in the $html variable has been converted yet.
293      *
294      *  @var boolean $_converted
295      *  @access private
296      *  @see $html, $text
297      */
298     var $_converted = false;
299
300     /**
301      *  Contains URL addresses from links to be rendered in plain text.
302      *
303      *  @var string $_link_list
304      *  @access private
305      *  @see _build_link_list()
306      */
307     var $_link_list = '';
308     
309     /**
310      *  Number of valid links detected in the text, used for plain text
311      *  display (rendered similar to footnotes).
312      *
313      *  @var integer $_link_count
314      *  @access private
315      *  @see _build_link_list()
316      */
317     var $_link_count = 0;
318
319     /** 
320      * Boolean flag, true if a table of link URLs should be listed after the text. 
321      *  
322      * @var boolean $_do_links 
323      * @access private 
324      * @see html2text() 
325      */
326     var $_do_links = true;
327  
328     /**
329      *  Constructor.
330      *
331      *  If the HTML source string (or file) is supplied, the class
332      *  will instantiate with that source propagated, all that has
333      *  to be done it to call get_text().
334      *
335      *  @param string $source HTML content
336      *  @param boolean $from_file Indicates $source is a file to pull content from
337      *  @param boolean $do_links Indicate whether a table of link URLs is desired
338      *  @param integer $width Maximum width of the formatted text, 0 for no limit
339      *  @access public
340      *  @return void
341      */
342     function html2text( $source = '', $from_file = false, $do_links = true, $width = 75 )
343     {
344         if ( !empty($source) ) {
345             $this->set_html($source, $from_file);
346         }
347         
348         $this->set_base_url();
349         $this->_do_links = $do_links;
350         $this->width = $width;
351     }
352
353     /**
354      *  Loads source HTML into memory, either from $source string or a file.
355      *
356      *  @param string $source HTML content
357      *  @param boolean $from_file Indicates $source is a file to pull content from
358      *  @access public
359      *  @return void
360      */
361     function set_html( $source, $from_file = false )
362     {
363         if ( $from_file && file_exists($source) ) {
364             $this->html = file_get_contents($source); 
365         }
366         else
367             $this->html = $source;
368
369         $this->_converted = false;
370     }
371
372     /**
373      *  Returns the text, converted from HTML.
374      *
375      *  @access public
376      *  @return string
377      */
378     function get_text()
379     {
380         if ( !$this->_converted ) {
381             $this->_convert();
382         }
383
384         return $this->text;
385     }
386
387     /**
388      *  Prints the text, converted from HTML.
389      *
390      *  @access public
391      *  @return void
392      */
393     function print_text()
394     {
395         print $this->get_text();
396     }
397
398     /**
399      *  Alias to print_text(), operates identically.
400      *
401      *  @access public
402      *  @return void
403      *  @see print_text()
404      */
405     function p()
406     {
407         print $this->get_text();
408     }
409
410     /**
411      *  Sets the allowed HTML tags to pass through to the resulting text.
412      *
413      *  Tags should be in the form "<p>", with no corresponding closing tag.
414      *
415      *  @access public
416      *  @return void
417      */
418     function set_allowed_tags( $allowed_tags = '' )
419     {
420         if ( !empty($allowed_tags) ) {
421             $this->allowed_tags = $allowed_tags;
422         }
423     }
424
425     /**
426      *  Sets a base URL to handle relative links.
427      *
428      *  @access public
429      *  @return void
430      */
431     function set_base_url( $url = '' )
432     {
433         if ( empty($url) ) {
434                 if ( !empty($_SERVER['HTTP_HOST']) ) {
435                     $this->url = 'http://' . $_SERVER['HTTP_HOST'];
436                 } else {
437                     $this->url = '';
438                 }
439         } else {
440             // Strip any trailing slashes for consistency (relative
441             // URLs may already start with a slash like "/file.html")
442             if ( substr($url, -1) == '/' ) {
443                 $url = substr($url, 0, -1);
444             }
445             $this->url = $url;
446         }
447     }
448
449     /**
450      *  Workhorse function that does actual conversion.
451      *
452      *  First performs custom tag replacement specified by $search and
453      *  $replace arrays. Then strips any remaining HTML tags, reduces whitespace
454      *  and newlines to a readable format, and word wraps the text to
455      *  $width characters.
456      *
457      *  @access private
458      *  @return void
459      */
460     function _convert()
461     {
462         // Variables used for building the link list
463         $this->_link_count = 0;
464         $this->_link_list = '';
465
466         $text = trim(stripslashes($this->html));
467
468         // Convert <PRE>
469         $this->_convert_pre($text);
470
471         // Run our defined search-and-replace
472         $text = preg_replace($this->search, $this->replace, $text);
473         $text = preg_replace_callback($this->callback_search, array('html2text', '_preg_callback'), $text);
474
475         // Replace known html entities
476         $text = html_entity_decode($text, ENT_COMPAT, 'UTF-8');
477
478         // Strip any other HTML tags
479         $text = strip_tags($text, $this->allowed_tags);
480
481         // Bring down number of empty lines to 2 max
482         $text = preg_replace("/\n\s+\n/", "\n\n", $text);
483         $text = preg_replace("/[\n]{3,}/", "\n\n", $text);
484
485         // Add link list
486         if ( !empty($this->_link_list) ) {
487             $text .= "\n\nLinks:\n------\n" . $this->_link_list;
488         }
489
490         // Wrap the text to a readable format
491         // for PHP versions >= 4.0.2. Default width is 75
492         // If width is 0 or less, don't wrap the text.
493         if ( $this->width > 0 ) {
494                 $text = wordwrap($text, $this->width);
495         }
496
497         $this->text = $text;
498
499         $this->_converted = true;
500     }
501
502     /**
503      *  Helper function called by preg_replace() on link replacement.
504      *
505      *  Maintains an internal list of links to be displayed at the end of the
506      *  text, with numeric indices to the original point in the text they
507      *  appeared. Also makes an effort at identifying and handling absolute
508      *  and relative links.
509      *
510      *  @param string $link URL of the link
511      *  @param string $display Part of the text to associate number with
512      *  @access private
513      *  @return string
514      */
515     function _build_link_list( $link, $display )
516     {
517         if ( !$this->_do_links ) return $display;
518         
519         if ( substr($link, 0, 7) == 'http://' || substr($link, 0, 8) == 'https://' ||
520              substr($link, 0, 7) == 'mailto:' ) {
521             $this->_link_count++;
522             $this->_link_list .= "[" . $this->_link_count . "] $link\n";
523             $additional = ' [' . $this->_link_count . ']';
524                 } elseif ( substr($link, 0, 11) == 'javascript:' ) {
525                         // Don't count the link; ignore it
526                         $additional = '';
527                 // what about href="#anchor" ?
528         } else {
529             $this->_link_count++;
530             $this->_link_list .= "[" . $this->_link_count . "] " . $this->url;
531             if ( substr($link, 0, 1) != '/' ) {
532                 $this->_link_list .= '/';
533             }
534             $this->_link_list .= "$link\n";
535             $additional = ' [' . $this->_link_count . ']';
536         }
537
538         return $display . $additional;
539     }
540     
541     /**
542      *  Helper function for PRE body conversion.
543      *
544      *  @param string HTML content
545      *  @access private
546      */
547     function _convert_pre(&$text)
548     {
549         while(preg_match('/<pre[^>]*>(.*)<\/pre>/ismU', $text, $matches))
550         {
551             $result = preg_replace($this->pre_search, $this->pre_replace, $matches[1]);
552             $text = preg_replace('/<pre[^>]*>.*<\/pre>/ismU', '<div><br>' . $result . '<br></div>', $text, 1);
553         }
554     }
555
556     /**
557      *  Callback function for preg_replace_callback use.
558      *
559      *  @param  array PREG matches
560      *  @return string
561      *  @access private
562      */
563     function _preg_callback($matches)
564     {
565         switch($matches[1])
566         {
567             case 'b':
568             case 'strong':
569                 return $this->_strtoupper($matches[2]);
570             case 'hr':
571                 return $this->_strtoupper("\t\t". $matches[2] ."\n");
572             case 'h':
573                 return $this->_strtoupper("\n\n". $matches[2] ."\n\n");
574             case 'a':
575                 return $this->_build_link_list($matches[3], $matches[4]);
576         }
577     }
578     
579     /**
580      *  Strtoupper multibyte wrapper function
581      *
582      *  @param  string
583      *  @return string
584      *  @access private
585      */
586     function _strtoupper($str)
587     {
588         if (function_exists('mb_strtoupper'))
589             return mb_strtoupper($str);
590         else
591             return strtoupper($str);
592     }
593 }
594
595 ?>