Improved display of plain text messages and text to HTML conversion (#1488937)
Now instead of <pre> we use <div class="pre"> styled with monospace font. We replace whitespace characters with non-breaking spaces where needed. I.e. plain text is always unwrappable, until it uses format=flowed, in such a case only flowed paragraphs are wrappable. Also conversion of text to HTML in compose editor was modified in the same way.pull/191/head
parent
638ebf69c4
commit
eda92ed4c0
@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| Copyright (C) 2008-2014, The Roundcube Dev Team |
|
||||
| |
|
||||
| Licensed under the GNU General Public License version 3 or |
|
||||
| any later version with exceptions for skins & plugins. |
|
||||
| See the README file for a full license statement. |
|
||||
| |
|
||||
| PURPOSE: |
|
||||
| Converts plain text to HTML |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Converts plain text to HTML
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Utils
|
||||
*/
|
||||
class rcube_text2html
|
||||
{
|
||||
/**
|
||||
* Contains the HTML content after conversion.
|
||||
*
|
||||
* @var string $html
|
||||
*/
|
||||
protected $html;
|
||||
|
||||
/**
|
||||
* Contains the plain text.
|
||||
*
|
||||
* @var string $text
|
||||
*/
|
||||
protected $text;
|
||||
|
||||
/**
|
||||
* Configuration
|
||||
*
|
||||
* @var array $config
|
||||
*/
|
||||
protected $config = array(
|
||||
// non-breaking space
|
||||
'space' => "\xC2\xA0",
|
||||
// enables format=flowed parser
|
||||
'flowed' => false,
|
||||
// enables wrapping for non-flowed text
|
||||
'wrap' => false,
|
||||
// line-break tag
|
||||
'break' => "<br>\n",
|
||||
// prefix and suffix (wrapper element)
|
||||
'begin' => '<div class="pre">',
|
||||
'end' => '</end>',
|
||||
// enables links replacement
|
||||
'links' => true,
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* If the plain text source string (or file) is supplied, the class
|
||||
* will instantiate with that source propagated, all that has
|
||||
* to be done it to call get_html().
|
||||
*
|
||||
* @param string $source Plain text
|
||||
* @param boolean $from_file Indicates $source is a file to pull content from
|
||||
* @param array $config Class configuration
|
||||
*/
|
||||
function __construct($source = '', $from_file = false, $config = array())
|
||||
{
|
||||
if (!empty($source)) {
|
||||
$this->set_text($source, $from_file);
|
||||
}
|
||||
|
||||
if (!empty($config) && is_array($config)) {
|
||||
$this->config = array_merge($this->config, $config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads source text into memory, either from $source string or a file.
|
||||
*
|
||||
* @param string $source Plain text
|
||||
* @param boolean $from_file Indicates $source is a file to pull content from
|
||||
*/
|
||||
function set_text($source, $from_file = false)
|
||||
{
|
||||
if ($from_file && file_exists($source)) {
|
||||
$this->text = file_get_contents($source);
|
||||
}
|
||||
else {
|
||||
$this->text = $source;
|
||||
}
|
||||
|
||||
$this->_converted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTML content.
|
||||
*
|
||||
* @return string HTML content
|
||||
*/
|
||||
function get_html()
|
||||
{
|
||||
if (!$this->_converted) {
|
||||
$this->_convert();
|
||||
}
|
||||
|
||||
return $this->html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the HTML.
|
||||
*/
|
||||
function print_html()
|
||||
{
|
||||
print $this->get_html();
|
||||
}
|
||||
|
||||
/**
|
||||
* Workhorse function that does actual conversion (calls _converter() method).
|
||||
*/
|
||||
protected function _convert()
|
||||
{
|
||||
$text = stripslashes($this->text);
|
||||
|
||||
// Convert TXT to HTML
|
||||
$this->html = $this->_converter($text);
|
||||
$this->_converted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workhorse function that does actual conversion.
|
||||
*
|
||||
* @param string Plain text
|
||||
*/
|
||||
protected function _converter($text)
|
||||
{
|
||||
// make links and email-addresses clickable
|
||||
$attribs = array('link_attribs' => array('rel' => 'noreferrer', 'target' => '_blank'));
|
||||
$replacer = new rcmail_string_replacer($attribs);
|
||||
|
||||
if ($this->config['flowed']) {
|
||||
$flowed_char = 0x01;
|
||||
$text = rcube_mime::unfold_flowed($text, chr($flowed_char));
|
||||
}
|
||||
|
||||
// search for patterns like links and e-mail addresses and replace with tokens
|
||||
if ($this->config['links']) {
|
||||
$text = $replacer->replace($text);
|
||||
}
|
||||
|
||||
// split body into single lines
|
||||
$text = preg_split('/\r?\n/', $text);
|
||||
$quote_level = 0;
|
||||
$last = -1;
|
||||
|
||||
// find/mark quoted lines...
|
||||
for ($n=0, $cnt=count($text); $n < $cnt; $n++) {
|
||||
$flowed = false;
|
||||
if ($this->config['flowed'] && ord($text[0]) == $flowed_char) {
|
||||
$flowed = true;
|
||||
$text[$n] = substr($text[$n], 1);
|
||||
}
|
||||
|
||||
if ($text[$n][0] == '>' && preg_match('/^(>+ {0,1})+/', $text[$n], $regs)) {
|
||||
$q = substr_count($regs[0], '>');
|
||||
$text[$n] = substr($text[$n], strlen($regs[0]));
|
||||
$text[$n] = $this->_convert_line($text[$n], $flowed || $this->config['wrap']);
|
||||
|
||||
if ($q > $quote_level) {
|
||||
$text[$n] = $replacer->get_replacement($replacer->add(
|
||||
str_repeat('<blockquote>', $q - $quote_level))) . $text[$n];
|
||||
$last = $n;
|
||||
}
|
||||
else if ($q < $quote_level) {
|
||||
$text[$n] = $replacer->get_replacement($replacer->add(
|
||||
str_repeat('</blockquote>', $quote_level - $q))) . $text[$n];
|
||||
$last = $n;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$text[$n] = $this->_convert_line($text[$n], $flowed || $this->config['wrap']);
|
||||
$q = 0;
|
||||
|
||||
if ($quote_level > 0) {
|
||||
$text[$n] = $replacer->get_replacement($replacer->add(
|
||||
str_repeat('</blockquote>', $quote_level))) . $text[$n];
|
||||
}
|
||||
}
|
||||
|
||||
$quote_level = $q;
|
||||
}
|
||||
|
||||
if ($quote_level > 0) {
|
||||
$text[$n] = $replacer->get_replacement($replacer->add(
|
||||
str_repeat('</blockquote>', $quote_level))) . $text[$n];
|
||||
}
|
||||
|
||||
$text = join("\n", $text);
|
||||
|
||||
// colorize signature (up to <sig_max_lines> lines)
|
||||
$len = strlen($text);
|
||||
$sig_max_lines = rcube::get_instance()->config->get('sig_max_lines', 15);
|
||||
|
||||
while (($sp = strrpos($text, "-- \n", $sp ? -$len+$sp-1 : 0)) !== false) {
|
||||
if ($sp == 0 || $text[$sp-1] == "\n") {
|
||||
// do not touch blocks with more that X lines
|
||||
if (substr_count($text, "\n", $sp) < $sig_max_lines) {
|
||||
$text = substr($text, 0, max(0, $sp))
|
||||
.'<span class="sig">'.substr($text, $sp).'</span>';
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// insert url/mailto links and citation tags
|
||||
$text = $replacer->resolve($text);
|
||||
|
||||
// replace \n before </blockquote>
|
||||
$text = str_replace("\n</blockquote>", "</blockquote>", $text);
|
||||
|
||||
// replace line breaks
|
||||
$text = str_replace("\n", $this->config['break'], $text);
|
||||
|
||||
return $this->config['begin'] . $text . $this->config['end'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts spaces in line of text
|
||||
*/
|
||||
protected function _convert_line($text, $is_flowed)
|
||||
{
|
||||
static $table;
|
||||
|
||||
if (empty($table)) {
|
||||
$table = get_html_translation_table(HTML_SPECIALCHARS);
|
||||
unset($table['?']);
|
||||
}
|
||||
|
||||
// skip signature separator
|
||||
if ($text == '-- ') {
|
||||
return $text;
|
||||
}
|
||||
|
||||
// replace HTML special characters
|
||||
$text = strtr($text, $table);
|
||||
|
||||
$nbsp = $this->config['space'];
|
||||
|
||||
// replace some whitespace characters
|
||||
$text = str_replace(array("\r", "\t"), array('', ' '), $text);
|
||||
|
||||
// replace spaces with non-breaking spaces
|
||||
if ($is_flowed) {
|
||||
$text = preg_replace_callback('/(^|[^ ])( +)/', function($matches) {
|
||||
if (!strlen($matches[2])) {
|
||||
return str_repeat($nbsp, strlen($matches[2]));
|
||||
}
|
||||
else {
|
||||
return $matches[1] . ' ' . str_repeat($nbsp, strlen($matches[2])-1);
|
||||
}
|
||||
}, $text);
|
||||
}
|
||||
else {
|
||||
$text = str_replace(' ', $nbsp, $text);
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
+-----------------------------------------------------------------------+
|
||||
| program/steps/utils/text2html.inc |
|
||||
| |
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| Copyright (C) 2005-2014, The Roundcube Dev Team |
|
||||
| |
|
||||
| Licensed under the GNU General Public License version 3 or |
|
||||
| any later version with exceptions for skins & plugins. |
|
||||
| See the README file for a full license statement. |
|
||||
| |
|
||||
| PURPOSE: |
|
||||
| Convert plain text to HTML |
|
||||
| |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
$text = stream_get_contents(fopen('php://input', 'r'));
|
||||
|
||||
$converter = new rcube_text2html($text, false, array('wrap' => true));
|
||||
|
||||
header('Content-Type: text/html; charset=' . RCUBE_CHARSET);
|
||||
print $converter->get_html();
|
||||
exit;
|
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Test class to test rcube_text2html class
|
||||
*
|
||||
* @package Tests
|
||||
*/
|
||||
class Framework_Text2Html extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
|
||||
/**
|
||||
* Data for test_text2html()
|
||||
*/
|
||||
function data_text2html()
|
||||
{
|
||||
$options = array(
|
||||
'begin' => '',
|
||||
'end' => '',
|
||||
'break' => '<br>',
|
||||
'links' => false,
|
||||
'flowed' => false,
|
||||
'space' => '_', // replace UTF-8 non-breaking space for simpler testing
|
||||
);
|
||||
|
||||
$data[] = array(" aaaa", "_aaaa", $options);
|
||||
$data[] = array("aaaa aaaa", "aaaa_aaaa", $options);
|
||||
$data[] = array("aaaa aaaa", "aaaa__aaaa", $options);
|
||||
$data[] = array("aaaa aaaa", "aaaa___aaaa", $options);
|
||||
$data[] = array("aaaa\taaaa", "aaaa____aaaa", $options);
|
||||
$data[] = array("aaaa\naaaa", "aaaa<br>aaaa", $options);
|
||||
$data[] = array("aaaa\n aaaa", "aaaa<br>_aaaa", $options);
|
||||
$data[] = array("aaaa\n aaaa", "aaaa<br>__aaaa", $options);
|
||||
$data[] = array("aaaa\n aaaa", "aaaa<br>___aaaa", $options);
|
||||
$data[] = array("\taaaa", "____aaaa", $options);
|
||||
$data[] = array("\naaaa", "<br>aaaa", $options);
|
||||
$data[] = array("\n aaaa", "<br>_aaaa", $options);
|
||||
$data[] = array("\n aaaa", "<br>__aaaa", $options);
|
||||
$data[] = array("\n aaaa", "<br>___aaaa", $options);
|
||||
$data[] = array("aaaa\n\nbbbb", "aaaa<br><br>bbbb", $options);
|
||||
$data[] = array(">aaaa \n>aaaa", "<blockquote>aaaa_<br>aaaa</blockquote>", $options);
|
||||
$data[] = array(">aaaa\n>aaaa", "<blockquote>aaaa<br>aaaa</blockquote>", $options);
|
||||
$data[] = array(">aaaa \n>bbbb\ncccc dddd", "<blockquote>aaaa_<br>bbbb</blockquote>cccc_dddd", $options);
|
||||
|
||||
$options['flowed'] = true;
|
||||
|
||||
$data[] = array(" aaaa", "aaaa", $options);
|
||||
$data[] = array("aaaa aaaa", "aaaa_aaaa", $options);
|
||||
$data[] = array("aaaa aaaa", "aaaa__aaaa", $options);
|
||||
$data[] = array("aaaa aaaa", "aaaa___aaaa", $options);
|
||||
$data[] = array("aaaa\taaaa", "aaaa____aaaa", $options);
|
||||
$data[] = array("aaaa\naaaa", "aaaa<br>aaaa", $options);
|
||||
$data[] = array("aaaa\n aaaa", "aaaa<br>aaaa", $options);
|
||||
$data[] = array("aaaa\n aaaa", "aaaa<br>_aaaa", $options);
|
||||
$data[] = array("aaaa\n aaaa", "aaaa<br>__aaaa", $options);
|
||||
$data[] = array("\taaaa", "____aaaa", $options);
|
||||
$data[] = array("\naaaa", "<br>aaaa", $options);
|
||||
$data[] = array("\n aaaa", "<br>aaaa", $options);
|
||||
$data[] = array("\n aaaa", "<br>_aaaa", $options);
|
||||
$data[] = array("\n aaaa", "<br>__aaaa", $options);
|
||||
$data[] = array("aaaa\n\nbbbb", "aaaa<br><br>bbbb", $options);
|
||||
$data[] = array(">aaaa \n>aaaa", "<blockquote>aaaa aaaa</blockquote>", $options);
|
||||
$data[] = array(">aaaa\n>aaaa", "<blockquote>aaaa<br>aaaa</blockquote>", $options);
|
||||
$data[] = array(">aaaa \n>bbbb\ncccc dddd", "<blockquote>aaaa bbbb</blockquote>cccc_dddd", $options);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test text to html conversion
|
||||
*
|
||||
* @dataProvider data_text2html
|
||||
*/
|
||||
function test_text2html($input, $output, $options)
|
||||
{
|
||||
$t2h = new rcube_text2html($input, false, $options);
|
||||
|
||||
$html = $t2h->get_html();
|
||||
|
||||
$this->assertEquals($output, $html);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue