source: github/program/lib/html2text.php @ 5a74dc8

HEADcourier-fixdev-browser-capabilitiespdorelease-0.6release-0.7release-0.8
Last change on this file since 5a74dc8 was 5a74dc8, checked in by alecpl <alec@…>, 3 years ago
  • Fix regular expression for HTML entities
  • Property mode set to 100644
File size: 20.2 KB
Line 
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 */
107class 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        '/<p[^>]*>/i',                           // <P>
152        '/<br[^>]*>/i',                          // <br>
153        '/<i[^>]*>(.*?)<\/i>/i',                 // <i>
154        '/<em[^>]*>(.*?)<\/em>/i',               // <em>
155        '/(<ul[^>]*>|<\/ul>)/i',                 // <ul> and </ul>
156        '/(<ol[^>]*>|<\/ol>)/i',                 // <ol> and </ol>
157        '/<li[^>]*>(.*?)<\/li>/i',               // <li> and </li>
158        '/<li[^>]*>/i',                          // <li>
159        '/<hr[^>]*>/i',                          // <hr>
160        '/<div[^>]*>/i',                         // <div>
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        '/[ ]{2,}/'                              // Runs of spaces, post-handling
180    );
181
182    /**
183     *  List of pattern replacements corresponding to patterns searched.
184     *
185     *  @var array $replace
186     *  @access public
187     *  @see $search
188     */
189    var $replace = array(
190        '',                                     // Non-legal carriage return
191        ' ',                                    // Newlines and tabs
192        ' ',                                    // Runs of spaces, pre-handling
193        '',                                     // <script>s -- which strip_tags supposedly has problems with
194        '',                                     // <style>s -- which strip_tags supposedly has problems with
195        "\n\n",                                 // <P>
196        "\n",                                   // <br>
197        '_\\1_',                                // <i>
198        '_\\1_',                                // <em>
199        "\n\n",                                 // <ul> and </ul>
200        "\n\n",                                 // <ol> and </ol>
201        "\t* \\1\n",                            // <li> and </li>
202        "\n\t* ",                               // <li>
203        "\n-------------------------\n",        // <hr>
204        "<div>\n",                                   // <div>
205        "\n\n",                                 // <table> and </table>
206        "\n",                                   // <tr> and </tr>
207        "\t\t\\1\n",                            // <td> and </td>
208        ' ',                                    // Non-breaking space
209        '"',                                    // Double quotes
210        "'",                                    // Single quotes
211        '>',
212        '<',
213        '&',
214        '(c)',
215        '(tm)',
216        '(R)',
217        '--',
218        '-',
219        '*',
220        '£',
221        'EUR',                                  // Euro sign. € ?
222        ' '                                     // Runs of spaces, post-handling
223    );
224
225    /**
226     *  List of preg* regular expression patterns to search for
227     *  and replace using callback function.
228     *
229     *  @var array $callback_search
230     *  @access public
231     */
232    var $callback_search = array(
233        '/<(h)[123456][^>]*>(.*?)<\/h[123456]>/i', // H1 - H3
234        '/<(b)[^>]*>(.*?)<\/b>/i',                 // <b>
235        '/<(strong)[^>]*>(.*?)<\/strong>/i',       // <strong>
236        '/<(a) [^>]*href=("|\')([^"\']+)\2[^>]*>(.*?)<\/a>/i',
237                                                   // <a href="">
238        '/<(th)[^>]*>(.*?)<\/th>/i',               // <th> and </th>
239    );
240
241   /**
242    *  List of preg* regular expression patterns to search for in PRE body,
243    *  used in conjunction with $pre_replace.
244    *
245    *  @var array $pre_search
246    *  @access public
247    *  @see $pre_replace
248    */
249    var $pre_search = array(
250        "/\n/",
251        "/\t/",
252        '/ /',
253        '/<pre[^>]*>/',
254        '/<\/pre>/'
255    );
256
257    /**
258     *  List of pattern replacements corresponding to patterns searched for PRE body.
259     *
260     *  @var array $pre_replace
261     *  @access public
262     *  @see $pre_search
263     */
264    var $pre_replace = array(
265        '<br>',
266        '&nbsp;&nbsp;&nbsp;&nbsp;',
267        '&nbsp;',
268        '',
269        ''
270    );
271
272    /**
273     *  Contains a list of HTML tags to allow in the resulting text.
274     *
275     *  @var string $allowed_tags
276     *  @access public
277     *  @see set_allowed_tags()
278     */
279    var $allowed_tags = '';
280
281    /**
282     *  Contains the base URL that relative links should resolve to.
283     *
284     *  @var string $url
285     *  @access public
286     */
287    var $url;
288
289    /**
290     *  Indicates whether content in the $html variable has been converted yet.
291     *
292     *  @var boolean $_converted
293     *  @access private
294     *  @see $html, $text
295     */
296    var $_converted = false;
297
298    /**
299     *  Contains URL addresses from links to be rendered in plain text.
300     *
301     *  @var string $_link_list
302     *  @access private
303     *  @see _build_link_list()
304     */
305    var $_link_list = '';
306   
307    /**
308     *  Number of valid links detected in the text, used for plain text
309     *  display (rendered similar to footnotes).
310     *
311     *  @var integer $_link_count
312     *  @access private
313     *  @see _build_link_list()
314     */
315    var $_link_count = 0;
316
317    /**
318     * Boolean flag, true if a table of link URLs should be listed after the text.
319     * 
320     * @var boolean $_do_links
321     * @access private
322     * @see html2text()
323     */
324    var $_do_links = true;
325 
326    /**
327     *  Constructor.
328     *
329     *  If the HTML source string (or file) is supplied, the class
330     *  will instantiate with that source propagated, all that has
331     *  to be done it to call get_text().
332     *
333     *  @param string $source HTML content
334     *  @param boolean $from_file Indicates $source is a file to pull content from
335     *  @param boolean $do_links Indicate whether a table of link URLs is desired
336     *  @param integer $width Maximum width of the formatted text, 0 for no limit
337     *  @access public
338     *  @return void
339     */
340    function html2text( $source = '', $from_file = false, $do_links = true, $width = 75 )
341    {
342        if ( !empty($source) ) {
343            $this->set_html($source, $from_file);
344        }
345
346        $this->set_base_url();
347        $this->_do_links = $do_links;
348        $this->width = $width;
349    }
350
351    /**
352     *  Loads source HTML into memory, either from $source string or a file.
353     *
354     *  @param string $source HTML content
355     *  @param boolean $from_file Indicates $source is a file to pull content from
356     *  @access public
357     *  @return void
358     */
359    function set_html( $source, $from_file = false )
360    {
361        if ( $from_file && file_exists($source) ) {
362            $this->html = file_get_contents($source); 
363        }
364        else
365            $this->html = $source;
366
367        $this->_converted = false;
368    }
369
370    /**
371     *  Returns the text, converted from HTML.
372     *
373     *  @access public
374     *  @return string
375     */
376    function get_text()
377    {
378        if ( !$this->_converted ) {
379            $this->_convert();
380        }
381
382        return $this->text;
383    }
384
385    /**
386     *  Prints the text, converted from HTML.
387     *
388     *  @access public
389     *  @return void
390     */
391    function print_text()
392    {
393        print $this->get_text();
394    }
395
396    /**
397     *  Alias to print_text(), operates identically.
398     *
399     *  @access public
400     *  @return void
401     *  @see print_text()
402     */
403    function p()
404    {
405        print $this->get_text();
406    }
407
408    /**
409     *  Sets the allowed HTML tags to pass through to the resulting text.
410     *
411     *  Tags should be in the form "<p>", with no corresponding closing tag.
412     *
413     *  @access public
414     *  @return void
415     */
416    function set_allowed_tags( $allowed_tags = '' )
417    {
418        if ( !empty($allowed_tags) ) {
419            $this->allowed_tags = $allowed_tags;
420        }
421    }
422
423    /**
424     *  Sets a base URL to handle relative links.
425     *
426     *  @access public
427     *  @return void
428     */
429    function set_base_url( $url = '' )
430    {
431        if ( empty($url) ) {
432                if ( !empty($_SERVER['HTTP_HOST']) ) {
433                    $this->url = 'http://' . $_SERVER['HTTP_HOST'];
434                } else {
435                    $this->url = '';
436                }
437        } else {
438            // Strip any trailing slashes for consistency (relative
439            // URLs may already start with a slash like "/file.html")
440            if ( substr($url, -1) == '/' ) {
441                $url = substr($url, 0, -1);
442            }
443            $this->url = $url;
444        }
445    }
446
447    /**
448     *  Workhorse function that does actual conversion.
449     *
450     *  First performs custom tag replacement specified by $search and
451     *  $replace arrays. Then strips any remaining HTML tags, reduces whitespace
452     *  and newlines to a readable format, and word wraps the text to
453     *  $width characters.
454     *
455     *  @access private
456     *  @return void
457     */
458    function _convert()
459    {
460        // Variables used for building the link list
461        $this->_link_count = 0;
462        $this->_link_list = '';
463
464        $text = trim(stripslashes($this->html));
465
466        // Convert <PRE>
467        $this->_convert_pre($text);
468
469        // Run our defined search-and-replace
470        $text = preg_replace($this->search, $this->replace, $text);
471
472        // Replace known html entities
473        $text = html_entity_decode($text, ENT_COMPAT, 'UTF-8');
474
475        // Run our defined search-and-replace with callback
476        $text = preg_replace_callback($this->callback_search, array('html2text', '_preg_callback'), $text);
477
478        // Remove unknown/unhandled entities (this cannot be done in search-and-replace block)
479        $text = preg_replace('/&#?[a-z0-9]{2,7};/i', '', $text); 
480
481        // Strip any other HTML tags
482        $text = strip_tags($text, $this->allowed_tags);
483
484        // Bring down number of empty lines to 2 max
485        $text = preg_replace("/\n\s+\n/", "\n\n", $text);
486        $text = preg_replace("/[\n]{3,}/", "\n\n", $text);
487
488        // Add link list
489        if ( !empty($this->_link_list) ) {
490            $text .= "\n\nLinks:\n------\n" . $this->_link_list;
491        }
492
493        // Wrap the text to a readable format
494        // for PHP versions >= 4.0.2. Default width is 75
495        // If width is 0 or less, don't wrap the text.
496        if ( $this->width > 0 ) {
497                $text = wordwrap($text, $this->width);
498        }
499
500        $this->text = $text;
501
502        $this->_converted = true;
503    }
504
505    /**
506     *  Helper function called by preg_replace() on link replacement.
507     *
508     *  Maintains an internal list of links to be displayed at the end of the
509     *  text, with numeric indices to the original point in the text they
510     *  appeared. Also makes an effort at identifying and handling absolute
511     *  and relative links.
512     *
513     *  @param string $link URL of the link
514     *  @param string $display Part of the text to associate number with
515     *  @access private
516     *  @return string
517     */
518    function _build_link_list( $link, $display )
519    {
520        if ( !$this->_do_links ) return $display;
521       
522        if ( substr($link, 0, 7) == 'http://' || substr($link, 0, 8) == 'https://' ||
523             substr($link, 0, 7) == 'mailto:' ) {
524            $this->_link_count++;
525            $this->_link_list .= "[" . $this->_link_count . "] $link\n";
526            $additional = ' [' . $this->_link_count . ']';
527        } elseif ( substr($link, 0, 11) == 'javascript:' ) {
528                // Don't count the link; ignore it
529                $additional = '';
530                // what about href="#anchor" ?
531        } else {
532            $this->_link_count++;
533            $this->_link_list .= "[" . $this->_link_count . "] " . $this->url;
534            if ( substr($link, 0, 1) != '/' ) {
535                $this->_link_list .= '/';
536            }
537            $this->_link_list .= "$link\n";
538            $additional = ' [' . $this->_link_count . ']';
539        }
540
541        return $display . $additional;
542    }
543   
544    /**
545     *  Helper function for PRE body conversion.
546     *
547     *  @param string HTML content
548     *  @access private
549     */
550    function _convert_pre(&$text)
551    {
552        while(preg_match('/<pre[^>]*>(.*)<\/pre>/ismU', $text, $matches)) {
553            $result = preg_replace($this->pre_search, $this->pre_replace, $matches[1]);
554            $text = preg_replace('/<pre[^>]*>.*<\/pre>/ismU', '<div><br>' . $result . '<br></div>', $text, 1);
555        }
556    }
557
558    /**
559     *  Callback function for preg_replace_callback use.
560     *
561     *  @param  array PREG matches
562     *  @return string
563     *  @access private
564     */
565    function _preg_callback($matches)
566    {
567        switch($matches[1]) {
568        case 'b':
569        case 'strong':
570            return $this->_strtoupper($matches[2]);
571        case 'th':
572            return $this->_strtoupper("\t\t". $matches[2] ."\n");
573        case 'h':
574            return $this->_strtoupper("\n\n". $matches[2] ."\n\n");
575        case 'a':
576            return $this->_build_link_list($matches[3], $matches[4]);
577        }
578    }
579   
580    /**
581     *  Strtoupper multibyte wrapper function
582     *
583     *  @param  string
584     *  @return string
585     *  @access private
586     */
587    function _strtoupper($str)
588    {
589        if (function_exists('mb_strtoupper'))
590            return mb_strtoupper($str);
591        else
592            return strtoupper($str);
593    }
594}
595
596?>
Note: See TracBrowser for help on using the repository browser.