source: github/program/lib/html2text.php @ 300fc65

HEADcourier-fixdev-browser-capabilitiespdorelease-0.6release-0.7release-0.8
Last change on this file since 300fc65 was 300fc65, checked in by alecpl <alec@…>, 5 years ago
  • Better HTML entities conversion in html2text (#1485519)
  • Property mode set to 100644
File size: 19.3 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        //'/<!-- .* -->/',                         // Comments -- which strip_tags might have problem a with
152        '/<h[123][^>]*>(.*?)<\/h[123]>/ie',      // H1 - H3
153        '/<h[456][^>]*>(.*?)<\/h[456]>/ie',      // H4 - H6
154        '/<p[^>]*>/i',                           // <P>
155        '/<br[^>]*>/i',                          // <br>
156        '/<b[^>]*>(.*?)<\/b>/ie',                // <b>
157        '/<strong[^>]*>(.*?)<\/strong>/ie',      // <strong>
158        '/<i[^>]*>(.*?)<\/i>/i',                 // <i>
159        '/<em[^>]*>(.*?)<\/em>/i',               // <em>
160        '/(<ul[^>]*>|<\/ul>)/i',                 // <ul> and </ul>
161        '/(<ol[^>]*>|<\/ol>)/i',                 // <ol> and </ol>
162        '/<li[^>]*>(.*?)<\/li>/i',               // <li> and </li>
163        '/<li[^>]*>/i',                          // <li>
164        '/<a [^>]*href=("|\')([^"\']+)\1[^>]*>(.*?)<\/a>/ie',
165                                                 // <a href="">
166        '/<hr[^>]*>/i',                          // <hr>
167        '/(<table[^>]*>|<\/table>)/i',           // <table> and </table>
168        '/(<tr[^>]*>|<\/tr>)/i',                 // <tr> and </tr>
169        '/<td[^>]*>(.*?)<\/td>/i',               // <td> and </td>
170        '/<th[^>]*>(.*?)<\/th>/ie',              // <th> and </th>
171        '/&(nbsp|#160);/i',                      // Non-breaking space
172        '/&(quot|rdquo|ldquo|#8220|#8221|#147|#148);/i',
173                                                         // Double quotes
174        '/&(apos|rsquo|lsquo|#8216|#8217);/i',   // Single quotes
175        '/&gt;/i',                               // Greater-than
176        '/&lt;/i',                               // Less-than
177        '/&(amp|#38);/i',                        // Ampersand
178        '/&(copy|#169);/i',                      // Copyright
179        '/&(trade|#8482|#153);/i',               // Trademark
180        '/&(reg|#174);/i',                       // Registered
181        '/&(mdash|#151|#8212);/i',               // mdash
182        '/&(ndash|minus|#8211|#8722);/i',        // ndash
183        '/&(bull|#149|#8226);/i',                // Bullet
184        '/&(pound|#163);/i',                     // Pound sign
185        '/&(euro|#8364);/i',                     // Euro sign
186        '/&[^&;]+;/i',                           // Unknown/unhandled entities
187        '/[ ]{2,}/'                              // Runs of spaces, post-handling
188    );
189
190    /**
191     *  List of pattern replacements corresponding to patterns searched.
192     *
193     *  @var array $replace
194     *  @access public
195     *  @see $search
196     */
197    var $replace = array(
198        '',                                     // Non-legal carriage return
199        ' ',                                    // Newlines and tabs
200        ' ',                                    // Runs of spaces, pre-handling
201        '',                                     // <script>s -- which strip_tags supposedly has problems with
202        '',                                     // <style>s -- which strip_tags supposedly has problems with
203        //'',                                     // Comments -- which strip_tags might have problem a with
204        "strtoupper(\"\n\n\\1\n\n\")",          // H1 - H3
205        "ucwords(\"\n\n\\1\n\")",             // H4 - H6
206        "\n\n",                               // <P>
207        "\n",                                   // <br>
208        'strtoupper("\\1")',                    // <b>
209        'strtoupper("\\1")',                    // <strong>
210        '_\\1_',                                // <i>
211        '_\\1_',                                // <em>
212        "\n\n",                                 // <ul> and </ul>
213        "\n\n",                                 // <ol> and </ol>
214        "\t* \\1\n",                            // <li> and </li>
215        "\n\t* ",                               // <li>
216        '$this->_build_link_list("\\2", "\\3")',
217                                                // <a href="">
218        "\n-------------------------\n",        // <hr>
219        "\n\n",                                 // <table> and </table>
220        "\n",                                   // <tr> and </tr>
221        "\t\t\\1\n",                            // <td> and </td>
222        "strtoupper(\"\t\t\\1\n\")",            // <th> and </th>
223        ' ',                                    // Non-breaking space
224        '"',                                    // Double quotes
225        "'",                                    // Single quotes
226        '>',
227        '<',
228        '&',
229        '(c)',
230        '(tm)',
231        '(R)',
232        '--',
233        '-',
234        '*',
235        '£',
236        'EUR',                                  // Euro sign. € ?
237        '',                                     // Unknown/unhandled entities
238        ' '                                     // Runs of spaces, post-handling
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        // Replace known html entities
470        $text = html_entity_decode($text, ENT_COMPAT, 'UTF-8');
471
472        // Run our defined search-and-replace
473        $text = preg_replace($this->search, $this->replace, $text);
474
475        // Strip any other HTML tags
476        $text = strip_tags($text, $this->allowed_tags);
477
478        // Bring down number of empty lines to 2 max
479        $text = preg_replace("/\n\s+\n/", "\n\n", $text);
480        $text = preg_replace("/[\n]{3,}/", "\n\n", $text);
481
482        // Add link list
483        if ( !empty($this->_link_list) ) {
484            $text .= "\n\nLinks:\n------\n" . $this->_link_list;
485        }
486
487        // Wrap the text to a readable format
488        // for PHP versions >= 4.0.2. Default width is 75
489        // If width is 0 or less, don't wrap the text.
490        if ( $this->width > 0 ) {
491                $text = wordwrap($text, $this->width);
492        }
493
494        $this->text = $text;
495
496        $this->_converted = true;
497    }
498
499    /**
500     *  Helper function called by preg_replace() on link replacement.
501     *
502     *  Maintains an internal list of links to be displayed at the end of the
503     *  text, with numeric indices to the original point in the text they
504     *  appeared. Also makes an effort at identifying and handling absolute
505     *  and relative links.
506     *
507     *  @param string $link URL of the link
508     *  @param string $display Part of the text to associate number with
509     *  @access private
510     *  @return string
511     */
512    function _build_link_list( $link, $display )
513    {
514        if ( !$this->_do_links ) return $display;
515       
516        if ( substr($link, 0, 7) == 'http://' || substr($link, 0, 8) == 'https://' ||
517             substr($link, 0, 7) == 'mailto:' ) {
518            $this->_link_count++;
519            $this->_link_list .= "[" . $this->_link_count . "] $link\n";
520            $additional = ' [' . $this->_link_count . ']';
521                } elseif ( substr($link, 0, 11) == 'javascript:' ) {
522                        // Don't count the link; ignore it
523                        $additional = '';
524                // what about href="#anchor" ?
525        } else {
526            $this->_link_count++;
527            $this->_link_list .= "[" . $this->_link_count . "] " . $this->url;
528            if ( substr($link, 0, 1) != '/' ) {
529                $this->_link_list .= '/';
530            }
531            $this->_link_list .= "$link\n";
532            $additional = ' [' . $this->_link_count . ']';
533        }
534
535        return $display . $additional;
536    }
537   
538    /**
539     *  Helper function for PRE body conversion.
540     *
541     *  @param string HTML content
542     *  @access private
543     */
544    function _convert_pre(&$text)
545    {
546        while(preg_match('/<pre[^>]*>(.*)<\/pre>/ismU', $text, $matches))
547        {
548            $result = preg_replace($this->pre_search, $this->pre_replace, $matches[1]);
549            $text = preg_replace('/<pre[^>]*>.*<\/pre>/ismU', '<div><br>' . $result . '<br></div>', $text, 1);
550        }
551    }
552}
553
554?>
Note: See TracBrowser for help on using the repository browser.