Aleksander Machniak
2014-10-28 d93019125cca783e8acfaa68467024375321e55f
commit | author | age
4e17e6 1 <?php
T 2
66afd7 3 /**
AM 4  +-----------------------------------------------------------------------+
5  | This file is part of the Roundcube Webmail client                     |
6  | Copyright (C) 2008-2012, The Roundcube Dev Team                       |
7  | Copyright (c) 2005-2007, Jon Abernathy <jon@chuggnutt.com>            |
8  |                                                                       |
9  | Licensed under the GNU General Public License version 3 or            |
10  | any later version with exceptions for skins & plugins.                |
11  | See the README file for a full license statement.                     |
12  |                                                                       |
13  | PURPOSE:                                                              |
14  |   Converts HTML to formatted plain text (based on html2text class)    |
15  +-----------------------------------------------------------------------+
16  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
17  | Author: Aleksander Machniak <alec@alec.pl>                            |
18  | Author: Jon Abernathy <jon@chuggnutt.com>                             |
19  +-----------------------------------------------------------------------+
20  */
4e17e6 21
T 22 /**
8ac6fd 23  *  Takes HTML and converts it to formatted, plain text.
A 24  *
25  *  Thanks to Alexander Krug (http://www.krugar.de/) to pointing out and
26  *  correcting an error in the regexp search array. Fixed 7/30/03.
27  *
28  *  Updated set_html() function's file reading mechanism, 9/25/03.
29  *
30  *  Thanks to Joss Sanglier (http://www.dancingbear.co.uk/) for adding
31  *  several more HTML entity codes to the $search and $replace arrays.
32  *  Updated 11/7/03.
33  *
34  *  Thanks to Darius Kasperavicius (http://www.dar.dar.lt/) for
35  *  suggesting the addition of $allowed_tags and its supporting function
36  *  (which I slightly modified). Updated 3/12/04.
37  *
38  *  Thanks to Justin Dearing for pointing out that a replacement for the
39  *  <TH> tag was missing, and suggesting an appropriate fix.
40  *  Updated 8/25/04.
41  *
42  *  Thanks to Mathieu Collas (http://www.myefarm.com/) for finding a
43  *  display/formatting bug in the _build_link_list() function: email
44  *  readers would show the left bracket and number ("[1") as part of the
45  *  rendered email address.
46  *  Updated 12/16/04.
47  *
48  *  Thanks to Wojciech Bajon (http://histeria.pl/) for submitting code
49  *  to handle relative links, which I hadn't considered. I modified his
50  *  code a bit to handle normal HTTP links and MAILTO links. Also for
51  *  suggesting three additional HTML entity codes to search for.
52  *  Updated 03/02/05.
53  *
54  *  Thanks to Jacob Chandler for pointing out another link condition
55  *  for the _build_link_list() function: "https".
56  *  Updated 04/06/05.
57  *
58  *  Thanks to Marc Bertrand (http://www.dresdensky.com/) for
59  *  suggesting a revision to the word wrapping functionality; if you
60  *  specify a $width of 0 or less, word wrapping will be ignored.
61  *  Updated 11/02/06.
62  *
63  *  *** Big housecleaning updates below:
64  *
65  *  Thanks to Colin Brown (http://www.sparkdriver.co.uk/) for
66  *  suggesting the fix to handle </li> and blank lines (whitespace).
67  *  Christian Basedau (http://www.movetheweb.de/) also suggested the
68  *  blank lines fix.
69  *
70  *  Special thanks to Marcus Bointon (http://www.synchromedia.co.uk/),
71  *  Christian Basedau, Norbert Laposa (http://ln5.co.uk/),
72  *  Bas van de Weijer, and Marijn van Butselaar
73  *  for pointing out my glaring error in the <th> handling. Marcus also
74  *  supplied a host of fixes.
75  *
76  *  Thanks to Jeffrey Silverman (http://www.newtnotes.com/) for pointing
77  *  out that extra spaces should be compressed--a problem addressed with
78  *  Marcus Bointon's fixes but that I had not yet incorporated.
79  *
21d463 80  *  Thanks to Daniel Schledermann (http://www.typoconsult.dk/) for
8ac6fd 81  *  suggesting a valuable fix with <a> tag handling.
A 82  *
83  *  Thanks to Wojciech Bajon (again!) for suggesting fixes and additions,
84  *  including the <a> tag handling that Daniel Schledermann pointed
85  *  out but that I had not yet incorporated. I haven't (yet)
86  *  incorporated all of Wojciech's changes, though I may at some
87  *  future time.
88  *
89  *  *** End of the housecleaning updates. Updated 08/08/07.
90  */
66afd7 91
AM 92 /**
93  * Converts HTML to formatted plain text
94  *
95  * @package    Framework
96  * @subpackage Utils
97  */
98 class rcube_html2text
4e17e6 99 {
66afd7 100     /**
AM 101      * Contains the HTML content to convert.
102      *
103      * @var string $html
104      */
105     protected $html;
4e17e6 106
T 107     /**
66afd7 108      * Contains the converted, formatted text.
4e17e6 109      *
66afd7 110      * @var string $text
4e17e6 111      */
66afd7 112     protected $text;
4e17e6 113
T 114     /**
66afd7 115      * Maximum width of the formatted text, in columns.
4e17e6 116      *
66afd7 117      * Set this value to 0 (or less) to ignore word wrapping
AM 118      * and not constrain text to a fixed-width column.
119      *
120      * @var integer $width
4e17e6 121      */
66afd7 122     protected $width = 70;
4e17e6 123
T 124     /**
66afd7 125      * Target character encoding for output text
4e17e6 126      *
66afd7 127      * @var string $charset
4e17e6 128      */
66afd7 129     protected $charset = 'UTF-8';
4e17e6 130
T 131     /**
66afd7 132      * List of preg* regular expression patterns to search for,
AM 133      * used in conjunction with $replace.
c72a96 134      *
66afd7 135      * @var array $search
AM 136      * @see $replace
c72a96 137      */
66afd7 138     protected $search = array(
4e17e6 139         "/\r/",                                  // Non-legal carriage return
T 140         "/[\n\t]+/",                             // Newlines and tabs
dc8f29 141         '/<head[^>]*>.*?<\/head>/i',             // <head>
4e17e6 142         '/<script[^>]*>.*?<\/script>/i',         // <script>s -- which strip_tags supposedly has problems with
8ac6fd 143         '/<style[^>]*>.*?<\/style>/i',           // <style>s -- which strip_tags supposedly has problems with
4e17e6 144         '/<p[^>]*>/i',                           // <P>
T 145         '/<br[^>]*>/i',                          // <br>
8ac6fd 146         '/<i[^>]*>(.*?)<\/i>/i',                 // <i>
A 147         '/<em[^>]*>(.*?)<\/em>/i',               // <em>
4e17e6 148         '/(<ul[^>]*>|<\/ul>)/i',                 // <ul> and </ul>
T 149         '/(<ol[^>]*>|<\/ol>)/i',                 // <ol> and </ol>
8ac6fd 150         '/<li[^>]*>(.*?)<\/li>/i',               // <li> and </li>
4e17e6 151         '/<li[^>]*>/i',                          // <li>
T 152         '/<hr[^>]*>/i',                          // <hr>
f6b282 153         '/<div[^>]*>/i',                         // <div>
4e17e6 154         '/(<table[^>]*>|<\/table>)/i',           // <table> and </table>
T 155         '/(<tr[^>]*>|<\/tr>)/i',                 // <tr> and </tr>
8ac6fd 156         '/<td[^>]*>(.*?)<\/td>/i',               // <td> and </td>
4e17e6 157     );
T 158
159     /**
66afd7 160      * List of pattern replacements corresponding to patterns searched.
4e17e6 161      *
66afd7 162      * @var array $replace
AM 163      * @see $search
4e17e6 164      */
66afd7 165     protected $replace = array(
4e17e6 166         '',                                     // Non-legal carriage return
T 167         ' ',                                    // Newlines and tabs
dc8f29 168         '',                                     // <head>
4e17e6 169         '',                                     // <script>s -- which strip_tags supposedly has problems with
8ac6fd 170         '',                                     // <style>s -- which strip_tags supposedly has problems with
f6b282 171         "\n\n",                                 // <P>
4e17e6 172         "\n",                                   // <br>
T 173         '_\\1_',                                // <i>
8ac6fd 174         '_\\1_',                                // <em>
4e17e6 175         "\n\n",                                 // <ul> and </ul>
T 176         "\n\n",                                 // <ol> and </ol>
8ac6fd 177         "\t* \\1\n",                            // <li> and </li>
A 178         "\n\t* ",                               // <li>
6972cc 179         "\n-------------------------\n",        // <hr>
11bcac 180         "<div>\n",                              // <div>
6972cc 181         "\n\n",                                 // <table> and </table>
4e17e6 182         "\n",                                   // <tr> and </tr>
T 183         "\t\t\\1\n",                            // <td> and </td>
ca0cd0 184     );
A 185
186     /**
66afd7 187      * List of preg* regular expression patterns to search for,
AM 188      * used in conjunction with $ent_replace.
ca0cd0 189      *
66afd7 190      * @var array $ent_search
AM 191      * @see $ent_replace
ca0cd0 192      */
66afd7 193     protected $ent_search = array(
ca0cd0 194         '/&(nbsp|#160);/i',                      // Non-breaking space
A 195         '/&(quot|rdquo|ldquo|#8220|#8221|#147|#148);/i',
21d463 196                                          // Double quotes
ca0cd0 197         '/&(apos|rsquo|lsquo|#8216|#8217);/i',   // Single quotes
A 198         '/&gt;/i',                               // Greater-than
199         '/&lt;/i',                               // Less-than
200         '/&(copy|#169);/i',                      // Copyright
201         '/&(trade|#8482|#153);/i',               // Trademark
202         '/&(reg|#174);/i',                       // Registered
203         '/&(mdash|#151|#8212);/i',               // mdash
204         '/&(ndash|minus|#8211|#8722);/i',        // ndash
205         '/&(bull|#149|#8226);/i',                // Bullet
206         '/&(pound|#163);/i',                     // Pound sign
207         '/&(euro|#8364);/i',                     // Euro sign
208         '/&(amp|#38);/i',                        // Ampersand: see _converter()
209         '/[ ]{2,}/',                             // Runs of spaces, post-handling
210     );
211
212     /**
66afd7 213      * List of pattern replacements corresponding to patterns searched.
ca0cd0 214      *
66afd7 215      * @var array $ent_replace
AM 216      * @see $ent_search
ca0cd0 217      */
66afd7 218     protected $ent_replace = array(
8ac6fd 219         ' ',                                    // Non-breaking space
A 220         '"',                                    // Double quotes
221         "'",                                    // Single quotes
4e17e6 222         '>',
T 223         '<',
224         '(c)',
225         '(tm)',
226         '(R)',
8ac6fd 227         '--',
A 228         '-',
4e17e6 229         '*',
300fc6 230         '£',
8ac6fd 231         'EUR',                                  // Euro sign. Â€ ?
6084d7 232         '|+|amp|+|',                            // Ampersand: see _converter()
ca0cd0 233         ' ',                                    // Runs of spaces, post-handling
f50cc7 234     );
A 235
236     /**
66afd7 237      * List of preg* regular expression patterns to search for
AM 238      * and replace using callback function.
f50cc7 239      *
66afd7 240      * @var array $callback_search
f50cc7 241      */
66afd7 242     protected $callback_search = array(
8c1880 243         '/<(a) [^>]*href=("|\')([^"\']+)\2[^>]*>(.*?)<\/a>/i', // <a href="">
AM 244         '/<(h)[123456]( [^>]*)?>(.*?)<\/h[123456]>/i',         // h1 - h6
245         '/<(b)( [^>]*)?>(.*?)<\/b>/i',                         // <b>
246         '/<(strong)( [^>]*)?>(.*?)<\/strong>/i',               // <strong>
247         '/<(th)( [^>]*)?>(.*?)<\/th>/i',                       // <th> and </th>
7353fa 248     );
A 249
8ac6fd 250    /**
66afd7 251     * List of preg* regular expression patterns to search for in PRE body,
AM 252     * used in conjunction with $pre_replace.
8ac6fd 253     *
66afd7 254     * @var array $pre_search
AM 255     * @see $pre_replace
8ac6fd 256     */
66afd7 257     protected $pre_search = array(
6972cc 258         "/\n/",
T 259         "/\t/",
260         '/ /',
261         '/<pre[^>]*>/',
262         '/<\/pre>/'
7353fa 263     );
A 264
265     /**
66afd7 266      * List of pattern replacements corresponding to patterns searched for PRE body.
7353fa 267      *
66afd7 268      * @var array $pre_replace
AM 269      * @see $pre_search
7353fa 270      */
66afd7 271     protected $pre_replace = array(
6972cc 272         '<br>',
T 273         '&nbsp;&nbsp;&nbsp;&nbsp;',
274         '&nbsp;',
275         '',
276         ''
4e17e6 277     );
T 278
279     /**
66afd7 280      * Contains a list of HTML tags to allow in the resulting text.
4e17e6 281      *
66afd7 282      * @var string $allowed_tags
AM 283      * @see set_allowed_tags()
4e17e6 284      */
66afd7 285     protected $allowed_tags = '';
4e17e6 286
T 287     /**
66afd7 288      * Contains the base URL that relative links should resolve to.
4e17e6 289      *
66afd7 290      * @var string $url
4e17e6 291      */
66afd7 292     protected $url;
4e17e6 293
T 294     /**
66afd7 295      * Indicates whether content in the $html variable has been converted yet.
4e17e6 296      *
66afd7 297      * @var boolean $_converted
AM 298      * @see $html, $text
4e17e6 299      */
66afd7 300     protected $_converted = false;
4e17e6 301
T 302     /**
66afd7 303      * Contains URL addresses from links to be rendered in plain text.
4e17e6 304      *
66afd7 305      * @var array $_link_list
AM 306      * @see _build_link_list()
4e17e6 307      */
66afd7 308     protected $_link_list = array();
4e17e6 309
ca0cd0 310     /**
A 311      * Boolean flag, true if a table of link URLs should be listed after the text.
312      *
313      * @var boolean $_do_links
66afd7 314      * @see __construct()
e7f85b 315      */
66afd7 316     protected $_do_links = true;
ca0cd0 317
4e17e6 318     /**
66afd7 319      * Constructor.
4e17e6 320      *
66afd7 321      * If the HTML source string (or file) is supplied, the class
AM 322      * will instantiate with that source propagated, all that has
323      * to be done it to call get_text().
4e17e6 324      *
66afd7 325      * @param string $source HTML content
AM 326      * @param boolean $from_file Indicates $source is a file to pull content from
327      * @param boolean $do_links Indicate whether a table of link URLs is desired
328      * @param integer $width Maximum width of the formatted text, 0 for no limit
4e17e6 329      */
66afd7 330     function __construct($source = '', $from_file = false, $do_links = true, $width = 75, $charset = 'UTF-8')
4e17e6 331     {
66afd7 332         if (!empty($source)) {
4e17e6 333             $this->set_html($source, $from_file);
T 334         }
6972cc 335
4e17e6 336         $this->set_base_url();
66afd7 337
6972cc 338         $this->_do_links = $do_links;
66afd7 339         $this->width     = $width;
AM 340         $this->charset   = $charset;
4e17e6 341     }
T 342
343     /**
66afd7 344      * Loads source HTML into memory, either from $source string or a file.
4e17e6 345      *
66afd7 346      * @param string $source HTML content
AM 347      * @param boolean $from_file Indicates $source is a file to pull content from
4e17e6 348      */
66afd7 349     function set_html($source, $from_file = false)
4e17e6 350     {
66afd7 351         if ($from_file && file_exists($source)) {
8c1880 352             $this->html = file_get_contents($source);
4e17e6 353         }
66afd7 354         else {
6972cc 355             $this->html = $source;
66afd7 356         }
4e17e6 357
T 358         $this->_converted = false;
359     }
360
361     /**
66afd7 362      * Returns the text, converted from HTML.
4e17e6 363      *
66afd7 364      * @return string Plain text
4e17e6 365      */
T 366     function get_text()
367     {
66afd7 368         if (!$this->_converted) {
4e17e6 369             $this->_convert();
T 370         }
371
372         return $this->text;
373     }
374
375     /**
66afd7 376      * Prints the text, converted from HTML.
4e17e6 377      */
T 378     function print_text()
379     {
380         print $this->get_text();
381     }
382
383     /**
66afd7 384      * Sets the allowed HTML tags to pass through to the resulting text.
4e17e6 385      *
66afd7 386      * Tags should be in the form "<p>", with no corresponding closing tag.
4e17e6 387      */
66afd7 388     function set_allowed_tags($allowed_tags = '')
4e17e6 389     {
66afd7 390         if (!empty($allowed_tags)) {
4e17e6 391             $this->allowed_tags = $allowed_tags;
T 392         }
393     }
394
395     /**
66afd7 396      * Sets a base URL to handle relative links.
4e17e6 397      */
66afd7 398     function set_base_url($url = '')
4e17e6 399     {
66afd7 400         if (empty($url)) {
AM 401             if (!empty($_SERVER['HTTP_HOST'])) {
21d463 402                 $this->url = 'http://' . $_SERVER['HTTP_HOST'];
66afd7 403             }
AM 404             else {
21d463 405                 $this->url = '';
AM 406             }
66afd7 407         }
AM 408         else {
4e17e6 409             // Strip any trailing slashes for consistency (relative
T 410             // URLs may already start with a slash like "/file.html")
66afd7 411             if (substr($url, -1) == '/') {
4e17e6 412                 $url = substr($url, 0, -1);
T 413             }
414             $this->url = $url;
415         }
416     }
417
418     /**
66afd7 419      * Workhorse function that does actual conversion (calls _converter() method).
4e17e6 420      */
66afd7 421     protected function _convert()
4e17e6 422     {
T 423         // Variables used for building the link list
43c40f 424         $this->_link_list = array();
4e17e6 425
efc470 426         $text = $this->html;
11bcac 427
A 428         // Convert HTML to TXT
429         $this->_converter($text);
430
431         // Add link list
43c40f 432         if (!empty($this->_link_list)) {
A 433             $text .= "\n\nLinks:\n------\n";
434             foreach ($this->_link_list as $idx => $url) {
435                 $text .= '[' . ($idx+1) . '] ' . $url . "\n";
436             }
11bcac 437         }
A 438
66afd7 439         $this->text       = $text;
11bcac 440         $this->_converted = true;
A 441     }
442
443     /**
66afd7 444      * Workhorse function that does actual conversion.
11bcac 445      *
66afd7 446      * First performs custom tag replacement specified by $search and
AM 447      * $replace arrays. Then strips any remaining HTML tags, reduces whitespace
448      * and newlines to a readable format, and word wraps the text to
449      * $width characters.
11bcac 450      *
66afd7 451      * @param string Reference to HTML content string
11bcac 452      */
66afd7 453     protected function _converter(&$text)
11bcac 454     {
A 455         // Convert <BLOCKQUOTE> (before PRE!)
456         $this->_convert_blockquotes($text);
4e17e6 457
6972cc 458         // Convert <PRE>
8ac6fd 459         $this->_convert_pre($text);
300fc6 460
ca0cd0 461         // Run our defined tags search-and-replace
4e17e6 462         $text = preg_replace($this->search, $this->replace, $text);
ca0cd0 463
A 464         // Run our defined tags search-and-replace with callback
66afd7 465         $text = preg_replace_callback($this->callback_search, array($this, 'tags_preg_callback'), $text);
ca0cd0 466
A 467         // Strip any other HTML tags
468         $text = strip_tags($text, $this->allowed_tags);
469
470         // Run our defined entities/characters search-and-replace
471         $text = preg_replace($this->ent_search, $this->ent_replace, $text);
4e17e6 472
6972cc 473         // Replace known html entities
c72a96 474         $text = html_entity_decode($text, ENT_QUOTES, $this->charset);
4e0419 475
0ee632 476         // Replace unicode nbsp to regular spaces
TB 477         $text = preg_replace('/\xC2\xA0/', ' ', $text);
478
755900 479         // Remove unknown/unhandled entities (this cannot be done in search-and-replace block)
6084d7 480         $text = preg_replace('/&([a-zA-Z0-9]{2,6}|#[0-9]{2,4});/', '', $text);
A 481
482         // Convert "|+|amp|+|" into "&", need to be done after handling of unknown entities
483         // This properly handles situation of "&amp;quot;" in input string
484         $text = str_replace('|+|amp|+|', '&', $text);
755900 485
4e17e6 486         // Bring down number of empty lines to 2 max
8ac6fd 487         $text = preg_replace("/\n\s+\n/", "\n\n", $text);
4e17e6 488         $text = preg_replace("/[\n]{3,}/", "\n\n", $text);
T 489
4d7fbd 490         // remove leading empty lines (can be produced by eg. P tag on the beginning)
ca0cd0 491         $text = ltrim($text, "\n");
4d7fbd 492
4e17e6 493         // Wrap the text to a readable format
T 494         // for PHP versions >= 4.0.2. Default width is 75
8ac6fd 495         // If width is 0 or less, don't wrap the text.
A 496         if ( $this->width > 0 ) {
21d463 497             $text = wordwrap($text, $this->width);
8ac6fd 498         }
4e17e6 499     }
T 500
501     /**
66afd7 502      * Helper function called by preg_replace() on link replacement.
4e17e6 503      *
66afd7 504      * Maintains an internal list of links to be displayed at the end of the
AM 505      * text, with numeric indices to the original point in the text they
506      * appeared. Also makes an effort at identifying and handling absolute
507      * and relative links.
4e17e6 508      *
66afd7 509      * @param string $link URL of the link
AM 510      * @param string $display Part of the text to associate number with
8ac6fd 511      */
66afd7 512     protected function _build_link_list( $link, $display )
8ac6fd 513     {
21d463 514         if (!$this->_do_links || empty($link)) {
AM 515             return $display;
4e17e6 516         }
T 517
21d463 518         // Ignored link types
AM 519         if (preg_match('!^(javascript:|mailto:|#)!i', $link)) {
520             return $display;
521         }
522
523         if (preg_match('!^([a-z][a-z0-9.+-]+:)!i', $link)) {
43c40f 524             $url = $link;
A 525         }
526         else {
527             $url = $this->url;
528             if (substr($link, 0, 1) != '/') {
529                 $url .= '/';
530             }
531             $url .= "$link";
532         }
533
534         if (($index = array_search($url, $this->_link_list)) === false) {
535             $index = count($this->_link_list);
8c1880 536             $this->_link_list[] = $url;
43c40f 537         }
A 538
539         return $display . ' [' . ($index+1) . ']';
8ac6fd 540     }
11bcac 541
7353fa 542     /**
66afd7 543      * Helper function for PRE body conversion.
7353fa 544      *
66afd7 545      * @param string HTML content
8ac6fd 546      */
66afd7 547     protected function _convert_pre(&$text)
8ac6fd 548     {
d483cd 549         // get the content of PRE element
11bcac 550         while (preg_match('/<pre[^>]*>(.*)<\/pre>/ismU', $text, $matches)) {
8c1880 551             $this->pre_content = $matches[1];
AM 552
553             // Run our defined tags search-and-replace with callback
554             $this->pre_content = preg_replace_callback($this->callback_search,
66afd7 555                 array($this, 'tags_preg_callback'), $this->pre_content);
8c1880 556
d483cd 557             // convert the content
A 558             $this->pre_content = sprintf('<div><br>%s<br></div>',
8c1880 559                 preg_replace($this->pre_search, $this->pre_replace, $this->pre_content));
AM 560
d483cd 561             // replace the content (use callback because content can contain $0 variable)
8c1880 562             $text = preg_replace_callback('/<pre[^>]*>.*<\/pre>/ismU',
66afd7 563                 array($this, 'pre_preg_callback'), $text, 1);
8c1880 564
d483cd 565             // free memory
A 566             $this->pre_content = '';
11bcac 567         }
A 568     }
569
570     /**
66afd7 571      * Helper function for BLOCKQUOTE body conversion.
11bcac 572      *
66afd7 573      * @param string HTML content
11bcac 574      */
66afd7 575     protected function _convert_blockquotes(&$text)
11bcac 576     {
bb6f4b 577         $level = 0;
TB 578         $offset = 0;
579         while (($start = strpos($text, '<blockquote', $offset)) !== false) {
580             $offset = $start + 12;
581             do {
582                 $end = strpos($text, '</blockquote>', $offset);
583                 $next = strpos($text, '<blockquote', $offset);
584
585                 // nested <blockquote>, skip
586                 if ($next !== false && $next < $end) {
587                     $offset = $next + 12;
588                     $level++;
589                 }
590                 // nested </blockquote> tag
591                 if ($end !== false && $level > 0) {
592                     $offset = $end + 12;
11bcac 593                     $level--;
A 594                 }
bb6f4b 595                 // found matching end tag
TB 596                 else if ($end !== false && $level == 0) {
597                     $taglen = strpos($text, '>', $start) - $start;
598                     $startpos = $start + $taglen + 1;
599
600                     // get blockquote content
601                     $body = trim(substr($text, $startpos, $end - $startpos));
602
737b62 603                     // adjust text wrapping width
TB 604                     $p_width = $this->width;
605                     if ($this->width > 0) $this->width -= 2;
606
bb6f4b 607                     // replace content with inner blockquotes
TB 608                     $this->_converter($body);
609
737b62 610                     // resore text width
TB 611                     $this->width = $p_width;
612
bb6f4b 613                     // Add citation markers and create <pre> block
27a620 614                     $body = preg_replace_callback('/((?:^|\n)>*)([^\n]*)/', array($this, 'blockquote_citation_callback'), trim($body));
bb6f4b 615                     $body = '<pre>' . htmlspecialchars($body) . '</pre>';
TB 616
ff6de9 617                     $text = substr_replace($text, $body . "\n", $start, $end + 13 - $start);
bb6f4b 618                     $offset = 0;
ff6de9 619
bb6f4b 620                     break;
11bcac 621                 }
eecd9c 622                 // abort on invalid tag structure (e.g. no closing tag found)
TB 623                 else {
624                     break;
625                 }
ff6de9 626             }
AM 627             while ($end || $next);
6972cc 628         }
8ac6fd 629     }
f50cc7 630
A 631     /**
bb6f4b 632      * Callback function to correctly add citation markers for blockquote contents
TB 633      */
27a620 634     public function blockquote_citation_callback($m)
bb6f4b 635     {
ff6de9 636         $line  = ltrim($m[2]);
bb6f4b 637         $space = $line[0] == '>' ? '' : ' ';
ff6de9 638
bb6f4b 639         return $m[1] . '>' . $space . $line;
TB 640     }
641
642     /**
66afd7 643      * Callback function for preg_replace_callback use.
f50cc7 644      *
66afd7 645      * @param  array PREG matches
AM 646      * @return string
f50cc7 647      */
66afd7 648     public function tags_preg_callback($matches)
f50cc7 649     {
2d7b4f 650         switch (strtolower($matches[1])) {
6972cc 651         case 'b':
T 652         case 'strong':
8c1880 653             return $this->_toupper($matches[3]);
da1722 654         case 'th':
8c1880 655             return $this->_toupper("\t\t". $matches[3] ."\n");
6972cc 656         case 'h':
8c1880 657             return $this->_toupper("\n\n". $matches[3] ."\n\n");
6972cc 658         case 'a':
29c542 659             // Remove spaces in URL (#1487805)
A 660             $url = str_replace(' ', '', $matches[3]);
661             return $this->_build_link_list($url, $matches[4]);
6972cc 662         }
f50cc7 663     }
29c542 664
f50cc7 665     /**
66afd7 666      * Callback function for preg_replace_callback use in PRE content handler.
d483cd 667      *
66afd7 668      * @param array PREG matches
AM 669      * @return string
d483cd 670      */
66afd7 671     public function pre_preg_callback($matches)
d483cd 672     {
A 673         return $this->pre_content;
674     }
675
676     /**
f35995 677      * Strtoupper function with HTML tags and entities handling.
f50cc7 678      *
f35995 679      * @param string $str Text to convert
A 680      * @return string Converted text
681      */
682     private function _toupper($str)
683     {
684         // string can containg HTML tags
685         $chunks = preg_split('/(<[^>]*>)/', $str, null, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
686
687         // convert toupper only the text between HTML tags
688         foreach ($chunks as $idx => $chunk) {
689             if ($chunk[0] != '<') {
690                 $chunks[$idx] = $this->_strtoupper($chunk);
691             }
692         }
693
694         return implode($chunks);
695     }
696
697     /**
698      * Strtoupper multibyte wrapper function with HTML entities handling.
699      *
700      * @param string $str Text to convert
701      * @return string Converted text
f50cc7 702      */
d483cd 703     private function _strtoupper($str)
f50cc7 704     {
c72a96 705         $str = html_entity_decode($str, ENT_COMPAT, $this->charset);
66afd7 706         $str = mb_strtoupper($str);
c72a96 707         $str = htmlspecialchars($str, ENT_COMPAT, $this->charset);
67e592 708
A 709         return $str;
f50cc7 710     }
4e17e6 711 }