Aleksander Machniak
2016-05-08 afd090672c2f45f7d6ca32493846b887c667dacc
commit | author | age
eda92e 1 <?php
AM 2
a95874 3 /**
eda92e 4  +-----------------------------------------------------------------------+
AM 5  | This file is part of the Roundcube Webmail client                     |
6  | Copyright (C) 2008-2014, The Roundcube Dev Team                       |
7  |                                                                       |
8  | Licensed under the GNU General Public License version 3 or            |
9  | any later version with exceptions for skins & plugins.                |
10  | See the README file for a full license statement.                     |
11  |                                                                       |
12  | PURPOSE:                                                              |
13  |   Converts plain text to HTML                                         |
14  +-----------------------------------------------------------------------+
15  | Author: Aleksander Machniak <alec@alec.pl>                            |
16  +-----------------------------------------------------------------------+
17  */
18
19 /**
20  * Converts plain text to HTML
21  *
22  * @package    Framework
23  * @subpackage Utils
24  */
25 class rcube_text2html
26 {
27     /**
28      * Contains the HTML content after conversion.
29      *
30      * @var string $html
31      */
32     protected $html;
33
34     /**
35      * Contains the plain text.
36      *
37      * @var string $text
38      */
39     protected $text;
40
41     /**
42      * Configuration
43      *
44      * @var array $config
45      */
46     protected $config = array(
47         // non-breaking space
d86ff9 48         'space' => "\xC2\xA0",
eda92e 49         // enables format=flowed parser
AM 50         'flowed' => false,
51         // enables wrapping for non-flowed text
d86ff9 52         'wrap' => true,
eda92e 53         // line-break tag
d86ff9 54         'break' => "<br>\n",
eda92e 55         // prefix and suffix (wrapper element)
d86ff9 56         'begin' => '<div class="pre">',
AM 57         'end'   => '</div>',
eda92e 58         // enables links replacement
d86ff9 59         'links' => true,
9cc5a5 60         // string replacer class
AM 61         'replacer' => 'rcube_string_replacer',
c4ad7e 62         // prefix and suffix of unwrappable line
AM 63         'nobr_start' => '<span style="white-space:nowrap">',
64         'nobr_end'   => '</span>',
eda92e 65     );
AM 66
67
68     /**
69      * Constructor.
70      *
71      * If the plain text source string (or file) is supplied, the class
72      * will instantiate with that source propagated, all that has
73      * to be done it to call get_html().
74      *
75      * @param string  $source    Plain text
76      * @param boolean $from_file Indicates $source is a file to pull content from
77      * @param array   $config    Class configuration
78      */
79     function __construct($source = '', $from_file = false, $config = array())
80     {
81         if (!empty($source)) {
82             $this->set_text($source, $from_file);
83         }
84
85         if (!empty($config) && is_array($config)) {
86             $this->config = array_merge($this->config, $config);
87         }
88     }
89
90     /**
91      * Loads source text into memory, either from $source string or a file.
92      *
93      * @param string  $source    Plain text
94      * @param boolean $from_file Indicates $source is a file to pull content from
95      */
96     function set_text($source, $from_file = false)
97     {
98         if ($from_file && file_exists($source)) {
99             $this->text = file_get_contents($source);
100         }
101         else {
102             $this->text = $source;
103         }
104
105         $this->_converted = false;
106     }
107
108     /**
109      * Returns the HTML content.
110      *
111      * @return string HTML content
112      */
113     function get_html()
114     {
115         if (!$this->_converted) {
116             $this->_convert();
117         }
118
119         return $this->html;
120     }
121
122     /**
123      * Prints the HTML.
124      */
125     function print_html()
126     {
127         print $this->get_html();
128     }
129
130     /**
131      * Workhorse function that does actual conversion (calls _converter() method).
132      */
133     protected function _convert()
134     {
135         // Convert TXT to HTML
efc470 136         $this->html       = $this->_converter($this->text);
eda92e 137         $this->_converted = true;
AM 138     }
139
140     /**
141      * Workhorse function that does actual conversion.
142      *
143      * @param string Plain text
144      */
145     protected function _converter($text)
146     {
147         // make links and email-addresses clickable
148         $attribs  = array('link_attribs' => array('rel' => 'noreferrer', 'target' => '_blank'));
9cc5a5 149         $replacer = new $this->config['replacer']($attribs);
eda92e 150
AM 151         if ($this->config['flowed']) {
152             $flowed_char = 0x01;
153             $text        = rcube_mime::unfold_flowed($text, chr($flowed_char));
154         }
155
156         // search for patterns like links and e-mail addresses and replace with tokens
157         if ($this->config['links']) {
158             $text = $replacer->replace($text);
159         }
160
161         // split body into single lines
162         $text        = preg_split('/\r?\n/', $text);
163         $quote_level = 0;
c0a5aa 164         $last        = null;
eda92e 165
c0a5aa 166         // wrap quoted lines with <blockquote>
AM 167         for ($n = 0, $cnt = count($text); $n < $cnt; $n++) {
eda92e 168             $flowed = false;
d41367 169             if ($this->config['flowed'] && ord($text[$n][0]) == $flowed_char) {
eda92e 170                 $flowed   = true;
AM 171                 $text[$n] = substr($text[$n], 1);
172             }
173
174             if ($text[$n][0] == '>' && preg_match('/^(>+ {0,1})+/', $text[$n], $regs)) {
175                 $q        = substr_count($regs[0], '>');
176                 $text[$n] = substr($text[$n], strlen($regs[0]));
177                 $text[$n] = $this->_convert_line($text[$n], $flowed || $this->config['wrap']);
c0a5aa 178                 $_length  = strlen(str_replace(' ', '', $text[$n]));
eda92e 179
AM 180                 if ($q > $quote_level) {
c0a5aa 181                     if ($last !== null) {
AM 182                         $text[$last] .= (!$length ? "\n" : '')
183                             . $replacer->get_replacement($replacer->add(
184                                 str_repeat('<blockquote>', $q - $quote_level)))
185                             . $text[$n];
186
187                         unset($text[$n]);
188                     }
189                     else {
190                         $text[$n] = $replacer->get_replacement($replacer->add(
191                             str_repeat('<blockquote>', $q - $quote_level))) . $text[$n];
192
193                         $last = $n;
194                     }
eda92e 195                 }
AM 196                 else if ($q < $quote_level) {
c0a5aa 197                     $text[$last] .= (!$length ? "\n" : '')
AM 198                         . $replacer->get_replacement($replacer->add(
199                             str_repeat('</blockquote>', $quote_level - $q)))
200                         . $text[$n];
201
202                     unset($text[$n]);
203                 }
204                 else {
eda92e 205                     $last = $n;
AM 206                 }
207             }
208             else {
209                 $text[$n] = $this->_convert_line($text[$n], $flowed || $this->config['wrap']);
c0a5aa 210                 $q        = 0;
AM 211                 $_length  = strlen(str_replace(' ', '', $text[$n]));
eda92e 212
AM 213                 if ($quote_level > 0) {
c0a5aa 214                     $text[$last] .= (!$length ? "\n" : '')
AM 215                         . $replacer->get_replacement($replacer->add(
216                             str_repeat('</blockquote>', $quote_level)))
217                         . $text[$n];
218
219                     unset($text[$n]);
220                 }
221                 else {
222                     $last = $n;
eda92e 223                 }
AM 224             }
225
226             $quote_level = $q;
c0a5aa 227             $length      = $_length;
eda92e 228         }
AM 229
230         if ($quote_level > 0) {
c0a5aa 231             $text[$last] .= $replacer->get_replacement($replacer->add(
AM 232                 str_repeat('</blockquote>', $quote_level)));
eda92e 233         }
AM 234
235         $text = join("\n", $text);
236
237         // colorize signature (up to <sig_max_lines> lines)
c0a5aa 238         $len           = strlen($text);
AM 239         $sig_sep       = "--" . $this->config['space'] . "\n";
eda92e 240         $sig_max_lines = rcube::get_instance()->config->get('sig_max_lines', 15);
AM 241
c0a5aa 242         while (($sp = strrpos($text, $sig_sep, $sp ? -$len+$sp-1 : 0)) !== false) {
eda92e 243             if ($sp == 0 || $text[$sp-1] == "\n") {
AM 244                 // do not touch blocks with more that X lines
245                 if (substr_count($text, "\n", $sp) < $sig_max_lines) {
246                     $text = substr($text, 0, max(0, $sp))
247                         .'<span class="sig">'.substr($text, $sp).'</span>';
248                 }
249
250                 break;
251             }
252         }
253
254         // insert url/mailto links and citation tags
255         $text = $replacer->resolve($text);
256
257         // replace line breaks
258         $text = str_replace("\n", $this->config['break'], $text);
259
260         return $this->config['begin'] . $text . $this->config['end'];
261     }
262
263     /**
264      * Converts spaces in line of text
265      */
266     protected function _convert_line($text, $is_flowed)
267     {
268         static $table;
269
270         if (empty($table)) {
271             $table = get_html_translation_table(HTML_SPECIALCHARS);
272             unset($table['?']);
afd090 273
AM 274             // replace some whitespace characters
275             $table["\r"] = '';
276             $table["\t"] = '    ';
eda92e 277         }
AM 278
279         // skip signature separator
280         if ($text == '-- ') {
c0a5aa 281             return '--' . $this->config['space'];
eda92e 282         }
AM 283
afd090 284         // replace HTML special and whitespace characters
eda92e 285         $text = strtr($text, $table);
c4ad7e 286
AM 287         $nbsp = $this->config['space'];
eda92e 288
AM 289         // replace spaces with non-breaking spaces
290         if ($is_flowed) {
d9d276 291             $pos  = 0;
AM 292             $diff = 0;
293             $len  = strlen($nbsp);
294             $copy = $text;
295
296             while (($pos = strpos($text, ' ', $pos)) !== false) {
297                 if ($pos == 0 || $text[$pos-1] == ' ') {
298                     $copy = substr_replace($copy, $nbsp, $pos + $diff, 1);
299                     $diff += $len - 1;
300                 }
301                 $pos++;
302             }
303
304             $text = $copy;
eda92e 305         }
c4ad7e 306         // make the whole line non-breakable if needed
AM 307         else if ($text !== '' && preg_match('/[^a-zA-Z0-9_]/', $text)) {
308             // use non-breakable spaces to correctly display
309             // trailing/leading spaces and multi-space inside
310             $text = str_replace(' ', $nbsp, $text);
311             // wrap in nobr element, so it's not wrapped on e.g. - or /
312             $text = $this->config['nobr_start'] . $text .  $this->config['nobr_end'];
eda92e 313         }
AM 314
315         return $text;
316     }
317 }