Aleksander Machniak
2013-10-13 ff6de99ae4f353f793c95208ad6407ac59a63376
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
T 426         $text = trim(stripslashes($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
755900 476         // Remove unknown/unhandled entities (this cannot be done in search-and-replace block)
6084d7 477         $text = preg_replace('/&([a-zA-Z0-9]{2,6}|#[0-9]{2,4});/', '', $text);
A 478
479         // Convert "|+|amp|+|" into "&", need to be done after handling of unknown entities
480         // This properly handles situation of "&amp;quot;" in input string
481         $text = str_replace('|+|amp|+|', '&', $text);
755900 482
4e17e6 483         // Bring down number of empty lines to 2 max
8ac6fd 484         $text = preg_replace("/\n\s+\n/", "\n\n", $text);
4e17e6 485         $text = preg_replace("/[\n]{3,}/", "\n\n", $text);
T 486
4d7fbd 487         // remove leading empty lines (can be produced by eg. P tag on the beginning)
ca0cd0 488         $text = ltrim($text, "\n");
4d7fbd 489
4e17e6 490         // Wrap the text to a readable format
T 491         // for PHP versions >= 4.0.2. Default width is 75
8ac6fd 492         // If width is 0 or less, don't wrap the text.
A 493         if ( $this->width > 0 ) {
21d463 494             $text = wordwrap($text, $this->width);
8ac6fd 495         }
4e17e6 496     }
T 497
498     /**
66afd7 499      * Helper function called by preg_replace() on link replacement.
4e17e6 500      *
66afd7 501      * Maintains an internal list of links to be displayed at the end of the
AM 502      * text, with numeric indices to the original point in the text they
503      * appeared. Also makes an effort at identifying and handling absolute
504      * and relative links.
4e17e6 505      *
66afd7 506      * @param string $link URL of the link
AM 507      * @param string $display Part of the text to associate number with
8ac6fd 508      */
66afd7 509     protected function _build_link_list( $link, $display )
8ac6fd 510     {
21d463 511         if (!$this->_do_links || empty($link)) {
AM 512             return $display;
4e17e6 513         }
T 514
21d463 515         // Ignored link types
AM 516         if (preg_match('!^(javascript:|mailto:|#)!i', $link)) {
517             return $display;
518         }
519
520         if (preg_match('!^([a-z][a-z0-9.+-]+:)!i', $link)) {
43c40f 521             $url = $link;
A 522         }
523         else {
524             $url = $this->url;
525             if (substr($link, 0, 1) != '/') {
526                 $url .= '/';
527             }
528             $url .= "$link";
529         }
530
531         if (($index = array_search($url, $this->_link_list)) === false) {
532             $index = count($this->_link_list);
8c1880 533             $this->_link_list[] = $url;
43c40f 534         }
A 535
536         return $display . ' [' . ($index+1) . ']';
8ac6fd 537     }
11bcac 538
7353fa 539     /**
66afd7 540      * Helper function for PRE body conversion.
7353fa 541      *
66afd7 542      * @param string HTML content
8ac6fd 543      */
66afd7 544     protected function _convert_pre(&$text)
8ac6fd 545     {
d483cd 546         // get the content of PRE element
11bcac 547         while (preg_match('/<pre[^>]*>(.*)<\/pre>/ismU', $text, $matches)) {
8c1880 548             $this->pre_content = $matches[1];
AM 549
550             // Run our defined tags search-and-replace with callback
551             $this->pre_content = preg_replace_callback($this->callback_search,
66afd7 552                 array($this, 'tags_preg_callback'), $this->pre_content);
8c1880 553
d483cd 554             // convert the content
A 555             $this->pre_content = sprintf('<div><br>%s<br></div>',
8c1880 556                 preg_replace($this->pre_search, $this->pre_replace, $this->pre_content));
AM 557
d483cd 558             // replace the content (use callback because content can contain $0 variable)
8c1880 559             $text = preg_replace_callback('/<pre[^>]*>.*<\/pre>/ismU',
66afd7 560                 array($this, 'pre_preg_callback'), $text, 1);
8c1880 561
d483cd 562             // free memory
A 563             $this->pre_content = '';
11bcac 564         }
A 565     }
566
567     /**
66afd7 568      * Helper function for BLOCKQUOTE body conversion.
11bcac 569      *
66afd7 570      * @param string HTML content
11bcac 571      */
66afd7 572     protected function _convert_blockquotes(&$text)
11bcac 573     {
bb6f4b 574         $level = 0;
TB 575         $offset = 0;
576         while (($start = strpos($text, '<blockquote', $offset)) !== false) {
577             $offset = $start + 12;
578             do {
579                 $end = strpos($text, '</blockquote>', $offset);
580                 $next = strpos($text, '<blockquote', $offset);
581
582                 // nested <blockquote>, skip
583                 if ($next !== false && $next < $end) {
584                     $offset = $next + 12;
585                     $level++;
586                 }
587                 // nested </blockquote> tag
588                 if ($end !== false && $level > 0) {
589                     $offset = $end + 12;
11bcac 590                     $level--;
A 591                 }
bb6f4b 592                 // found matching end tag
TB 593                 else if ($end !== false && $level == 0) {
594                     $taglen = strpos($text, '>', $start) - $start;
595                     $startpos = $start + $taglen + 1;
596
597                     // get blockquote content
598                     $body = trim(substr($text, $startpos, $end - $startpos));
599
737b62 600                     // adjust text wrapping width
TB 601                     $p_width = $this->width;
602                     if ($this->width > 0) $this->width -= 2;
603
bb6f4b 604                     // replace content with inner blockquotes
TB 605                     $this->_converter($body);
606
737b62 607                     // resore text width
TB 608                     $this->width = $p_width;
609
bb6f4b 610                     // Add citation markers and create <pre> block
TB 611                     $body = preg_replace_callback('/((?:^|\n)>*)([^\n]*)/', array($this, 'blockquote_citation_ballback'), trim($body));
612                     $body = '<pre>' . htmlspecialchars($body) . '</pre>';
613
ff6de9 614                     $text = substr_replace($text, $body . "\n", $start, $end + 13 - $start);
bb6f4b 615                     $offset = 0;
ff6de9 616
bb6f4b 617                     break;
11bcac 618                 }
ff6de9 619             }
AM 620             while ($end || $next);
6972cc 621         }
8ac6fd 622     }
f50cc7 623
A 624     /**
bb6f4b 625      * Callback function to correctly add citation markers for blockquote contents
TB 626      */
627     public function blockquote_citation_ballback($m)
628     {
ff6de9 629         $line  = ltrim($m[2]);
bb6f4b 630         $space = $line[0] == '>' ? '' : ' ';
ff6de9 631
bb6f4b 632         return $m[1] . '>' . $space . $line;
TB 633     }
634
635     /**
66afd7 636      * Callback function for preg_replace_callback use.
f50cc7 637      *
66afd7 638      * @param  array PREG matches
AM 639      * @return string
f50cc7 640      */
66afd7 641     public function tags_preg_callback($matches)
f50cc7 642     {
2d7b4f 643         switch (strtolower($matches[1])) {
6972cc 644         case 'b':
T 645         case 'strong':
8c1880 646             return $this->_toupper($matches[3]);
da1722 647         case 'th':
8c1880 648             return $this->_toupper("\t\t". $matches[3] ."\n");
6972cc 649         case 'h':
8c1880 650             return $this->_toupper("\n\n". $matches[3] ."\n\n");
6972cc 651         case 'a':
29c542 652             // Remove spaces in URL (#1487805)
A 653             $url = str_replace(' ', '', $matches[3]);
654             return $this->_build_link_list($url, $matches[4]);
6972cc 655         }
f50cc7 656     }
29c542 657
f50cc7 658     /**
66afd7 659      * Callback function for preg_replace_callback use in PRE content handler.
d483cd 660      *
66afd7 661      * @param array PREG matches
AM 662      * @return string
d483cd 663      */
66afd7 664     public function pre_preg_callback($matches)
d483cd 665     {
A 666         return $this->pre_content;
667     }
668
669     /**
f35995 670      * Strtoupper function with HTML tags and entities handling.
f50cc7 671      *
f35995 672      * @param string $str Text to convert
A 673      * @return string Converted text
674      */
675     private function _toupper($str)
676     {
677         // string can containg HTML tags
678         $chunks = preg_split('/(<[^>]*>)/', $str, null, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
679
680         // convert toupper only the text between HTML tags
681         foreach ($chunks as $idx => $chunk) {
682             if ($chunk[0] != '<') {
683                 $chunks[$idx] = $this->_strtoupper($chunk);
684             }
685         }
686
687         return implode($chunks);
688     }
689
690     /**
691      * Strtoupper multibyte wrapper function with HTML entities handling.
692      *
693      * @param string $str Text to convert
694      * @return string Converted text
f50cc7 695      */
d483cd 696     private function _strtoupper($str)
f50cc7 697     {
c72a96 698         $str = html_entity_decode($str, ENT_COMPAT, $this->charset);
66afd7 699         $str = mb_strtoupper($str);
c72a96 700         $str = htmlspecialchars($str, ENT_COMPAT, $this->charset);
67e592 701
A 702         return $str;
f50cc7 703     }
4e17e6 704 }