2010-05-11 20:17:10 -04:00
/****************************************************************
* This file is distributed under the following license :
*
* Copyright ( c ) 2010 , Thomas Kister
*
* This program is free software ; you can redistribute it and / or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation ; either version 2
* of the License , or ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with this program ; if not , write to the Free Software
* Foundation , Inc . , 51 Franklin Street , Fifth Floor ,
* Boston , MA 02110 - 1301 , USA .
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
2012-09-18 18:59:23 -04:00
# include <QApplication>
2011-03-06 07:58:18 -05:00
# include <QTextBrowser>
2012-05-11 20:40:53 -04:00
# include <QtXml>
2012-09-18 18:59:23 -04:00
# include <QBuffer>
# include <QMessageBox>
2015-11-19 06:05:52 -05:00
# include <QTextDocumentFragment>
2012-09-18 18:59:23 -04:00
# include <qmath.h>
2015-12-06 20:28:27 -05:00
# include <QUrl>
2012-05-11 20:40:53 -04:00
2010-05-11 20:17:10 -04:00
# include "HandleRichText.h"
2011-05-01 18:26:41 -04:00
# include "gui/RetroShareLink.h"
2012-05-11 20:40:53 -04:00
# include "util/ObjectPainter.h"
2010-05-11 20:17:10 -04:00
2011-09-29 05:20:09 -04:00
# include <iostream>
2012-05-11 20:40:53 -04:00
/**
* The type of embedding we ' d like to do
*/
enum EmbeddedType
{
Ahref , ///< into <a></a>
Img ///< into <img/>
} ;
/**
* Base class for storing information about a given kind of embedding .
*
* Its only constructor is protected so it is impossible to instantiate it , and
* at the same time derived classes have to provide a type .
*/
class EmbedInHtml
{
protected :
EmbedInHtml ( EmbeddedType newType ) : myType ( newType ) { }
public :
const EmbeddedType myType ;
QRegExp myRE ;
} ;
2010-05-11 20:17:10 -04:00
2012-05-11 20:40:53 -04:00
/**
* This class is used to store information for embedding links into < a > < / a > tags .
*/
class EmbedInHtmlAhref : public EmbedInHtml
{
public :
EmbedInHtmlAhref ( ) : EmbedInHtml ( Ahref )
{
myRE . setPattern ( " ( \\ bretroshare://[^ \\ s]*)|( \\ bhttps?://[^ \\ s]*)|( \\ bfile://[^ \\ s]*)|( \\ bwww \\ .[^ \\ s]*) " ) ;
}
} ;
/**
* This class is used to store information for embedding smileys into < img / > tags .
*
* By default the QRegExp the variables are empty , which means it must be
* filled at runtime , typically when the smileys set is loaded . It can be
* either done by hand or by using one of the helper methods available .
*
* Note : The QHash uses only * one * smiley per key ( unlike soon - to - be - upgraded
* code out there ) .
*/
class EmbedInHtmlImg : public EmbedInHtml
{
public :
EmbedInHtmlImg ( ) : EmbedInHtml ( Img ) { }
QHash < QString , QString > smileys ;
} ;
2010-05-11 20:17:10 -04:00
2012-05-11 20:40:53 -04:00
/* global instance for embedding emoticons */
static EmbedInHtmlImg defEmbedImg ;
RsHtml : : RsHtml ( )
{
}
void RsHtml : : initEmoticons ( const QHash < QString , QString > & hash )
2010-05-11 20:17:10 -04:00
{
QString newRE ;
for ( QHash < QString , QString > : : const_iterator it = hash . begin ( ) ; it ! = hash . end ( ) ; + + it )
foreach ( QString smile , it . key ( ) . split ( " | " ) ) {
2010-09-15 10:32:09 -04:00
if ( smile . isEmpty ( ) ) {
continue ;
}
2012-05-11 20:40:53 -04:00
defEmbedImg . smileys . insert ( smile , it . value ( ) ) ;
2010-05-11 20:17:10 -04:00
newRE + = " ( " + QRegExp : : escape ( smile ) + " )| " ;
}
newRE . chop ( 1 ) ; // remove last |
2012-05-11 20:40:53 -04:00
defEmbedImg . myRE . setPattern ( newRE ) ;
}
bool RsHtml : : canReplaceAnchor ( QDomDocument & /*doc*/ , QDomElement & /*element*/ , const RetroShareLink & link )
{
switch ( link . type ( ) ) {
case RetroShareLink : : TYPE_UNKNOWN :
case RetroShareLink : : TYPE_FILE :
case RetroShareLink : : TYPE_PERSON :
case RetroShareLink : : TYPE_FORUM :
case RetroShareLink : : TYPE_CHANNEL :
case RetroShareLink : : TYPE_SEARCH :
case RetroShareLink : : TYPE_MESSAGE :
2013-06-10 09:27:24 -04:00
case RetroShareLink : : TYPE_EXTRAFILE :
case RetroShareLink : : TYPE_PRIVATE_CHAT :
case RetroShareLink : : TYPE_PUBLIC_MSG :
2012-05-11 20:40:53 -04:00
// not yet implemented
break ;
case RetroShareLink : : TYPE_CERTIFICATE :
return true ;
}
return false ;
}
void RsHtml : : anchorTextForImg ( QDomDocument & /*doc*/ , QDomElement & /*element*/ , const RetroShareLink & link , QString & text )
{
text = link . niceName ( ) ;
}
void RsHtml : : anchorStylesheetForImg ( QDomDocument & /*doc*/ , QDomElement & /*element*/ , const RetroShareLink & link , QString & styleSheet )
{
switch ( link . type ( ) ) {
case RetroShareLink : : TYPE_UNKNOWN :
case RetroShareLink : : TYPE_FILE :
case RetroShareLink : : TYPE_PERSON :
case RetroShareLink : : TYPE_FORUM :
case RetroShareLink : : TYPE_CHANNEL :
case RetroShareLink : : TYPE_SEARCH :
case RetroShareLink : : TYPE_MESSAGE :
2013-06-10 09:27:24 -04:00
case RetroShareLink : : TYPE_EXTRAFILE :
case RetroShareLink : : TYPE_PRIVATE_CHAT :
case RetroShareLink : : TYPE_PUBLIC_MSG :
2012-05-11 20:40:53 -04:00
// not yet implemented
break ;
case RetroShareLink : : TYPE_CERTIFICATE :
2012-05-12 19:44:48 -04:00
styleSheet = " QPushButton{ border-image: url(:/images/btn_blue.png) ;border-width: 4;padding: 0px 6px;font-size: 12px;color: white;} QPushButton:hover{ border-image: url(:/images/btn_blue_hover.png) ;} " ;
2012-05-11 20:40:53 -04:00
break ;
}
}
void RsHtml : : replaceAnchorWithImg ( QDomDocument & doc , QDomElement & element , QTextDocument * textDocument , const RetroShareLink & link )
{
if ( ! textDocument ) {
return ;
}
if ( ! link . valid ( ) ) {
return ;
}
if ( element . childNodes ( ) . length ( ) ! = 1 ) {
return ;
}
if ( ! canReplaceAnchor ( doc , element , link ) ) {
return ;
}
QString imgText ;
anchorTextForImg ( doc , element , link , imgText ) ;
QString styleSheet ;
anchorStylesheetForImg ( doc , element , link , styleSheet ) ;
QDomNode childNode = element . firstChild ( ) ;
/* build resource name */
QString resourceName = QString ( " %1_%2.png " ) . arg ( link . type ( ) ) . arg ( imgText ) ;
if ( ! textDocument - > resource ( QTextDocument : : ImageResource , QUrl ( resourceName ) ) . isValid ( ) ) {
/* draw a button on a pixmap */
QPixmap pixmap ;
ObjectPainter : : drawButton ( imgText , styleSheet , pixmap ) ;
/* add the image to the resource cache of the text document */
textDocument - > addResource ( QTextDocument : : ImageResource , QUrl ( resourceName ) , QVariant ( pixmap ) ) ;
}
element . removeChild ( childNode ) ;
/* replace text of the anchor with <img> */
QDomElement img = doc . createElement ( " img " ) ;
img . setAttribute ( " src " , resourceName ) ;
element . appendChild ( img ) ;
2010-05-11 20:17:10 -04:00
}
/**
* Parses a DOM tree and replaces text by HTML tags .
* The tree is traversed depth - first , but only through children of Element type
* nodes . Any other kind of node is terminal .
*
* If the node is of type Text , its data is checked against the user - provided
* regular expression . If there is a match , the text is cut in three parts : the
* preceding part that will be inserted before , the part to be replaced , and the
* following part which will be itself checked against the regular expression .
*
* The part to be replaced is sent to a user - provided functor that will create
* the necessary embedding and return a new Element node to be inserted .
*
* @ param [ in ] doc The whole DOM tree , necessary to create new nodes
* @ param [ in , out ] currentElement The current node ( which is of type Element )
* @ param [ in ] embedInfos The regular expression and the type of embedding to use
*/
2012-05-11 20:40:53 -04:00
void RsHtml : : embedHtml ( QTextDocument * textDocument , QDomDocument & doc , QDomElement & currentElement , EmbedInHtml & embedInfos , ulong flag )
2010-05-11 20:17:10 -04:00
{
if ( embedInfos . myRE . pattern ( ) . length ( ) = = 0 ) // we'll get stuck with an empty regexp
return ;
QDomNodeList children = currentElement . childNodes ( ) ;
for ( uint index = 0 ; index < children . length ( ) ; index + + ) {
2011-05-01 18:26:41 -04:00
QDomNode node = children . item ( index ) ;
if ( node . isElement ( ) ) {
2010-05-11 20:17:10 -04:00
// child is an element, we skip it if it's an <a> tag
2011-05-01 18:26:41 -04:00
QDomElement element = node . toElement ( ) ;
if ( element . tagName ( ) . toLower ( ) = = " head " ) {
// skip it
2015-12-26 09:57:46 -05:00
} else if ( element . tagName ( ) . toLower ( ) = = " style " ) {
// skip it
2011-05-01 18:26:41 -04:00
} else if ( element . tagName ( ) . toLower ( ) = = " a " ) {
2012-05-11 20:40:53 -04:00
// skip it
if ( embedInfos . myType = = Ahref ) {
// but add title if not available
if ( element . attribute ( " title " ) . isEmpty ( ) ) {
RetroShareLink link ( element . attribute ( " href " ) ) ;
if ( link . valid ( ) ) {
QString title = link . title ( ) ;
if ( ! title . isEmpty ( ) ) {
element . setAttribute ( " title " , title ) ;
}
if ( textDocument & & ( flag & RSHTML_FORMATTEXT_REPLACE_LINKS ) ) {
replaceAnchorWithImg ( doc , element , textDocument , link ) ;
}
}
} else {
if ( textDocument & & ( flag & RSHTML_FORMATTEXT_REPLACE_LINKS ) ) {
RetroShareLink link ( element . attribute ( " href " ) ) ;
if ( link . valid ( ) ) {
replaceAnchorWithImg ( doc , element , textDocument , link ) ;
}
}
2011-05-01 18:26:41 -04:00
}
}
} else {
2012-05-11 20:40:53 -04:00
embedHtml ( textDocument , doc , element , embedInfos , flag ) ;
2011-05-01 18:26:41 -04:00
}
2010-05-11 20:17:10 -04:00
}
2011-05-01 18:26:41 -04:00
else if ( node . isText ( ) ) {
2010-05-11 20:17:10 -04:00
// child is a text, we parse it
2011-05-01 18:26:41 -04:00
QString tempText = node . toText ( ) . data ( ) ;
2010-05-11 20:17:10 -04:00
if ( embedInfos . myRE . indexIn ( tempText ) = = - 1 )
continue ;
// there is at least one link inside, we start replacing
int currentPos = 0 ;
int nextPos = 0 ;
while ( ( nextPos = embedInfos . myRE . indexIn ( tempText , currentPos ) ) ! = - 1 ) {
// if nextPos == 0 it means the text begins by a link
if ( nextPos > 0 ) {
QDomText textPart = doc . createTextNode ( tempText . mid ( currentPos , nextPos - currentPos ) ) ;
2011-05-01 18:26:41 -04:00
currentElement . insertBefore ( textPart , node ) ;
2010-05-11 20:17:10 -04:00
index + + ;
}
// inserted tag
QDomElement insertedTag ;
switch ( embedInfos . myType ) {
2011-05-01 18:26:41 -04:00
case Ahref :
{
2012-05-12 12:04:18 -04:00
insertedTag = doc . createElement ( " a " ) ;
insertedTag . setAttribute ( " href " , embedInfos . myRE . cap ( 0 ) ) ;
insertedTag . appendChild ( doc . createTextNode ( embedInfos . myRE . cap ( 0 ) ) ) ;
2011-05-01 18:26:41 -04:00
RetroShareLink link ( embedInfos . myRE . cap ( 0 ) ) ;
2012-05-11 20:40:53 -04:00
if ( link . valid ( ) ) {
QString title = link . title ( ) ;
if ( ! title . isEmpty ( ) ) {
insertedTag . setAttribute ( " title " , title ) ;
}
if ( textDocument & & ( flag & RSHTML_FORMATTEXT_REPLACE_LINKS ) ) {
replaceAnchorWithImg ( doc , insertedTag , textDocument , link ) ;
}
}
2011-05-01 18:26:41 -04:00
}
2010-05-11 20:17:10 -04:00
break ;
2011-05-01 18:26:41 -04:00
case Img :
2010-05-11 20:17:10 -04:00
{
2011-05-01 18:26:41 -04:00
insertedTag = doc . createElement ( " img " ) ;
2010-05-11 20:17:10 -04:00
const EmbedInHtmlImg & embedImg = static_cast < const EmbedInHtmlImg & > ( embedInfos ) ;
insertedTag . setAttribute ( " src " , embedImg . smileys [ embedInfos . myRE . cap ( 0 ) ] ) ;
}
break ;
}
2012-05-11 20:40:53 -04:00
2012-05-12 12:04:18 -04:00
currentElement . insertBefore ( insertedTag , node ) ;
index + + ;
2010-05-11 20:17:10 -04:00
currentPos = nextPos + embedInfos . myRE . matchedLength ( ) ;
}
// text after the last link, only if there's one, don't touch the index
2011-05-01 18:26:41 -04:00
// otherwise decrement the index because we're going to remove node
2010-05-11 20:17:10 -04:00
if ( currentPos < tempText . length ( ) ) {
QDomText textPart = doc . createTextNode ( tempText . mid ( currentPos ) ) ;
2011-05-01 18:26:41 -04:00
currentElement . insertBefore ( textPart , node ) ;
2010-05-11 20:17:10 -04:00
}
else
index - - ;
2011-05-01 18:26:41 -04:00
currentElement . removeChild ( node ) ;
2010-05-11 20:17:10 -04:00
}
}
}
2014-01-18 17:55:14 -05:00
/**
* Save space and tab out of bracket that XML loose .
*
* @ param [ in ] text The text to save space .
* @ return Text with space saved .
*/
static QString saveSpace ( const QString text )
{
QString savedSpaceText = text ;
bool outBrackets = false , echapChar = false ;
QString keyName = " " ;
bool getKeyName = false ;
2014-08-07 19:07:16 -04:00
bool firstOutBracket = false ;
2014-01-18 17:55:14 -05:00
for ( int i = 0 ; i < savedSpaceText . length ( ) ; i + + ) {
QChar cursChar = savedSpaceText . at ( i ) ;
if ( getKeyName | | ( ! outBrackets & & keyName . isEmpty ( ) ) ) {
if ( ( cursChar = = QLatin1Char ( ' ' ) ) | | ( cursChar = = QLatin1Char ( ' > ' ) ) ) {
getKeyName = keyName . isEmpty ( ) ;
} else {
keyName . append ( cursChar . toLower ( ) ) ;
}
}
2014-08-07 19:07:16 -04:00
2014-01-18 17:55:14 -05:00
if ( cursChar = = QLatin1Char ( ' > ' ) ) {
2014-08-07 19:07:16 -04:00
if ( ! echapChar & & i > 0 ) { outBrackets = true ; firstOutBracket = true ; }
} else if ( cursChar = = QLatin1Char ( ' \t ' ) ) {
if ( outBrackets & & firstOutBracket & & ( keyName ! = " style " ) ) savedSpaceText . replace ( i , 1 , " " ) ;
2014-01-18 17:55:14 -05:00
} else if ( cursChar = = QLatin1Char ( ' ' ) ) {
2014-08-07 19:07:16 -04:00
if ( outBrackets & & firstOutBracket & & ( keyName ! = " style " ) ) savedSpaceText . replace ( i , 1 , " " ) ;
2014-01-18 17:55:14 -05:00
} else if ( cursChar = = QChar ( 0xA0 ) ) {
2014-08-07 19:07:16 -04:00
if ( outBrackets & & firstOutBracket & & ( keyName ! = " style " ) ) savedSpaceText . replace ( i , 1 , " " ) ;
2014-01-18 17:55:14 -05:00
} else if ( cursChar = = QLatin1Char ( ' < ' ) ) {
if ( ! echapChar ) { outBrackets = false ; getKeyName = true ; keyName . clear ( ) ; }
2014-08-07 19:07:16 -04:00
} else firstOutBracket = false ;
2014-01-18 17:55:14 -05:00
echapChar = ( cursChar = = QLatin1Char ( ' \\ ' ) ) ;
2014-08-07 19:07:16 -04:00
2014-01-18 17:55:14 -05:00
}
return savedSpaceText ;
}
2015-09-07 14:19:52 -04:00
QString RsHtml : : formatText ( QTextDocument * textDocument , const QString & text , ulong flag , const QColor & backgroundColor , qreal desiredContrast , int desiredMinimumFontSize )
2011-05-15 16:21:14 -04:00
{
2011-06-07 18:28:07 -04:00
if ( flag = = 0 | | text . isEmpty ( ) ) {
2011-05-15 16:21:14 -04:00
// nothing to do
return text ;
}
2014-01-18 17:55:14 -05:00
QString formattedText = text ;
//remove all prepend char that make doc.setContent() fail
formattedText . remove ( 0 , text . indexOf ( " < " ) ) ;
// Save Space and Tab because doc loose it.
formattedText = saveSpace ( formattedText ) ;
QString errorMsg ; int errorLine ; int errorColumn ;
QDomDocument doc ;
if ( doc . setContent ( formattedText , & errorMsg , & errorLine , & errorColumn ) = = false ) {
2011-05-15 16:21:14 -04:00
// convert text with QTextBrowser
QTextBrowser textBrowser ;
textBrowser . setText ( text ) ;
2014-01-18 17:55:14 -05:00
formattedText = textBrowser . toHtml ( ) ;
formattedText . remove ( 0 , formattedText . indexOf ( " < " ) ) ;
formattedText = saveSpace ( formattedText ) ;
doc . setContent ( formattedText , & errorMsg , & errorLine , & errorColumn ) ;
2011-05-15 16:21:14 -04:00
}
QDomElement body = doc . documentElement ( ) ;
if ( flag & RSHTML_FORMATTEXT_EMBED_SMILEYS ) {
2012-05-11 20:40:53 -04:00
embedHtml ( textDocument , doc , body , defEmbedImg , flag ) ;
2011-05-15 16:21:14 -04:00
}
if ( flag & RSHTML_FORMATTEXT_EMBED_LINKS ) {
EmbedInHtmlAhref defEmbedAhref ;
2012-05-11 20:40:53 -04:00
embedHtml ( textDocument , doc , body , defEmbedAhref , flag ) ;
2011-05-15 16:21:14 -04:00
}
2014-01-18 17:55:14 -05:00
formattedText = doc . toString ( - 1 ) ; // -1 removes any annoying carriage return misinterpreted by QTextEdit
2012-03-31 11:20:19 -04:00
2012-09-21 17:23:14 -04:00
if ( flag & RSHTML_OPTIMIZEHTML_MASK ) {
2015-09-07 14:19:52 -04:00
optimizeHtml ( formattedText , flag , backgroundColor , desiredContrast , desiredMinimumFontSize ) ;
2012-04-24 11:34:22 -04:00
}
2012-01-30 19:06:24 -05:00
return formattedText ;
2011-05-15 16:21:14 -04:00
}
static void findElements ( QDomDocument & doc , QDomElement & currentElement , const QString & nodeName , const QString & nodeAttribute , QStringList & elements )
{
if ( nodeName . isEmpty ( ) ) {
return ;
}
QDomNodeList children = currentElement . childNodes ( ) ;
for ( uint index = 0 ; index < children . length ( ) ; index + + ) {
QDomNode node = children . item ( index ) ;
if ( node . isElement ( ) ) {
QDomElement element = node . toElement ( ) ;
if ( QString : : compare ( element . tagName ( ) , nodeName , Qt : : CaseInsensitive ) = = 0 ) {
if ( nodeAttribute . isEmpty ( ) ) {
// use text
elements . append ( element . text ( ) ) ;
} else {
QString attribute = element . attribute ( nodeAttribute ) ;
if ( attribute . isEmpty ( ) = = false ) {
elements . append ( attribute ) ;
}
}
continue ;
}
findElements ( doc , element , nodeName , nodeAttribute , elements ) ;
}
}
}
2012-05-11 20:40:53 -04:00
bool RsHtml : : findAnchors ( const QString & text , QStringList & urls )
2011-05-15 16:21:14 -04:00
{
2014-01-18 17:55:14 -05:00
QString errorMsg ; int errorLine ; int errorColumn ;
2011-05-15 16:21:14 -04:00
QDomDocument doc ;
2014-01-18 17:55:14 -05:00
if ( doc . setContent ( text , & errorMsg , & errorLine , & errorColumn ) = = false ) {
2012-05-11 20:40:53 -04:00
// convert text with QTextBrowser
QTextBrowser textBrowser ;
textBrowser . setText ( text ) ;
2014-01-18 17:55:14 -05:00
doc . setContent ( textBrowser . toHtml ( ) , & errorMsg , & errorLine , & errorColumn ) ;
2011-05-15 16:21:14 -04:00
}
QDomElement body = doc . documentElement ( ) ;
findElements ( doc , body , " a " , " href " , urls ) ;
return true ;
}
2012-01-30 19:06:24 -05:00
static void removeElement ( QDomElement & parentElement , QDomElement & element )
{
QDomNodeList children = element . childNodes ( ) ;
while ( children . length ( ) > 0 ) {
QDomNode childElement = element . removeChild ( children . item ( children . length ( ) - 1 ) ) ;
parentElement . insertAfter ( childElement , element ) ;
}
parentElement . removeChild ( element ) ;
}
2012-09-21 17:23:14 -04:00
static qreal linearizeColorComponent ( qreal v )
{
if ( v < = 0.03928 ) return v / 12.92 ;
else return pow ( ( v + 0.055 ) / 1.055 , 2.4 ) ;
}
static qreal getRelativeLuminance ( const QColor & c )
{
qreal r = linearizeColorComponent ( c . redF ( ) ) * 0.2126 ;
qreal g = linearizeColorComponent ( c . greenF ( ) ) * 0.7152 ;
qreal b = linearizeColorComponent ( c . blueF ( ) ) * 0.0722 ;
return r + g + b ;
}
/**
* @ brief Compute the contrast between two relative luminances .
* See : http : //www.w3.org/TR/2012/NOTE-WCAG20-TECHS-20120103/G18
* @ param lum1 , lum2 Relative luminances returned by getRelativeLuminance ( ) .
* @ return Contrast between 1 and 21.
*/
static qreal getContrastRatio ( qreal lum1 , qreal lum2 )
{
if ( lum2 > lum1 ) {
qreal t = lum1 ;
lum1 = lum2 ;
lum2 = t ;
}
return ( lum1 + 0.05 ) / ( lum2 + 0.05 ) ;
}
/**
* @ brief Find a color with the same hue that provides the desired contrast with bglum .
* @ param [ in , out ] val Name of the original color . Will be modified .
* @ param bglum Background ' s relative luminance as returned by getRelativeLuminance ( ) .
*/
static void findBestColor ( QString & val , qreal bglum , qreal desiredContrast )
{
2012-09-24 07:32:11 -04:00
# if QT_VERSION < 0x040600
// missing methods on class QColor
Q_UNUSED ( val ) ;
Q_UNUSED ( bglum ) ;
Q_UNUSED ( desiredContrast ) ;
# else
2012-09-21 17:23:14 -04:00
// Change text color to get a good contrast with the background
QColor c ( val ) ;
qreal lum = : : getRelativeLuminance ( c ) ;
// Keep text color darker/brighter than the bg if possible
qreal lowContrast = : : getContrastRatio ( bglum , 0.0 ) ;
qreal highContrast = : : getContrastRatio ( bglum , 1.0 ) ;
bool searchDown = ( lum < = bglum & & lowContrast > = desiredContrast )
| | ( lum > bglum & & highContrast < desiredContrast & & lowContrast > = highContrast ) ;
// There's no such thing as too much contrast on a bright background,
// but on a dark background it creates haloing which makes text hard to read.
// So we enforce desired contrast when the bg is dark.
if ( ! searchDown | | : : getContrastRatio ( lum , bglum ) < desiredContrast ) {
// Bisection search of the correct "lightness" to get the desired contrast
qreal minl = searchDown ? 0.0 : bglum ;
qreal maxl = searchDown ? bglum : 1.0 ;
do {
QColor d = c ;
qreal midl = ( minl + maxl ) / 2.0 ;
d . setHslF ( c . hslHueF ( ) , c . hslSaturationF ( ) , midl ) ;
qreal lum = : : getRelativeLuminance ( d ) ;
if ( ( : : getContrastRatio ( lum , bglum ) < desiredContrast ) ^ searchDown ) {
minl = midl ;
}
else {
maxl = midl ;
}
} while ( maxl - minl > 0.01 ) ;
c . setHslF ( c . hslHueF ( ) , c . hslSaturationF ( ) , minl ) ;
val = c . name ( ) ;
}
2012-09-24 07:32:11 -04:00
# endif // QT_VERSION < 0x040600
2012-09-21 17:23:14 -04:00
}
2015-08-20 11:55:20 -04:00
/**
* @ brief optimizeHtml : Optimize HTML Text in DomDocument to reduce size
* @ param doc : QDomDocument containing Text to optimize
* @ param currentElement : Current element optimized
* @ param stylesList : List where to save all differents styles used in text
* @ param knownStyle : List of known styles
*/
static void optimizeHtml ( QDomDocument & doc
, QDomElement & currentElement
, QHash < QString , QStringList * > & stylesList
, QHash < QString , QString > & knownStyle )
2011-09-29 05:20:09 -04:00
{
2015-08-20 11:55:20 -04:00
if ( doc . documentElement ( ) . namedItem ( " style " ) . toElement ( ) . attributeNode ( " RSOptimized " ) . isAttr ( ) ) {
//Already optimized only get StyleList
QDomElement styleElem = doc . documentElement ( ) . namedItem ( " style " ) . toElement ( ) ;
if ( ! styleElem . isElement ( ) ) return ; //Not an element so a bad message.
QDomAttr styleAttr = styleElem . attributeNode ( " RSOptimized " ) ;
if ( ! styleAttr . isAttr ( ) ) return ; //Not an attribute so a bad message.
QString version = styleAttr . value ( ) ;
if ( version = = " v2 " ) {
QStringList allStyles = styleElem . text ( ) . split ( ' } ' ) ;
foreach ( QString style , allStyles ) {
QStringList pair = style . split ( ' { ' ) ;
if ( pair . length ( ) ! = 2 ) return ; //Malformed style list so a bad message or last item.
QString keyvalue = pair . at ( 1 ) ;
keyvalue . replace ( " ; " , " " ) ;
2016-01-01 06:17:08 -05:00
QStringList classUsingIt ( pair . at ( 0 ) . split ( ' , ' ) ) ;
2015-08-20 11:55:20 -04:00
QStringList * exported = new QStringList ( ) ;
2016-01-01 06:17:08 -05:00
foreach ( QString keyVal , classUsingIt ) {
2015-08-20 11:55:20 -04:00
if ( ! keyVal . trimmed ( ) . isEmpty ( ) ) {
exported - > append ( keyVal . trimmed ( ) . replace ( " . " , " " ) ) ;
}
}
stylesList . insert ( keyvalue , exported ) ;
}
}
return ;
}
2012-01-30 19:06:24 -05:00
if ( currentElement . tagName ( ) . toLower ( ) = = " html " ) {
// change <html> to <span>
currentElement . setTagName ( " span " ) ;
}
QDomNode styleNode ;
bool addBR = false ;
2011-09-29 05:20:09 -04:00
QDomNodeList children = currentElement . childNodes ( ) ;
for ( uint index = 0 ; index < children . length ( ) ; ) {
QDomNode node = children . item ( index ) ;
if ( node . isElement ( ) ) {
QDomElement element = node . toElement ( ) ;
2015-11-21 18:14:33 -05:00
styleNode = node . attributes ( ) . namedItem ( " style " ) ;
2012-01-30 19:06:24 -05:00
// not <p>
if ( addBR & & element . tagName ( ) . toLower ( ) ! = " p " ) {
// add <br> after a removed <p> but not before a <p>
QDomElement elementBr = doc . createElement ( " br " ) ;
currentElement . insertBefore ( elementBr , element ) ;
addBR = false ;
+ + index ;
}
// <body>
if ( element . tagName ( ) . toLower ( ) = = " body " ) {
if ( element . attributes ( ) . length ( ) = = 0 ) {
// remove <body> without attributes
removeElement ( currentElement , element ) ;
// no ++index;
continue ;
}
// change <body> to <span>
element . setTagName ( " span " ) ;
}
// <head>
2011-09-29 05:20:09 -04:00
if ( element . tagName ( ) . toLower ( ) = = " head " ) {
2012-01-30 19:06:24 -05:00
// remove <head>
2011-09-29 05:20:09 -04:00
currentElement . removeChild ( node ) ;
2012-01-30 19:06:24 -05:00
// no ++index;
2011-09-29 05:20:09 -04:00
continue ;
}
2012-01-30 19:06:24 -05:00
2015-12-06 20:28:27 -05:00
//hidden text in a
if ( element . tagName ( ) . toLower ( ) = = " a " ) {
if ( element . hasAttribute ( " href " ) ) {
QString href = element . attribute ( " href " , " " ) ;
if ( href . startsWith ( " hidden: " ) ) {
//this text should be hidden and appear in title
//we need this trick, because QTextEdit doesn't export the title attribute
QString title = href . remove ( 0 , QString ( " hidden: " ) . length ( ) ) ;
QString text = element . text ( ) ;
element . setTagName ( " span " ) ;
element . removeAttribute ( " href " ) ;
QDomNodeList c = element . childNodes ( ) ;
for ( int i = 0 ; i < c . count ( ) ; i + + ) {
element . removeChild ( c . at ( i ) ) ;
} ;
element . setAttribute ( QString ( " title " ) , title ) ;
QDomText textnode = doc . createTextNode ( text ) ;
element . appendChild ( textnode ) ;
}
}
}
2012-01-30 19:06:24 -05:00
// iterate children
2015-08-20 11:55:20 -04:00
optimizeHtml ( doc , element , stylesList , knownStyle ) ;
2012-01-30 19:06:24 -05:00
// <p>
if ( element . tagName ( ) . toLower ( ) = = " p " ) {
// <p style="...">
2015-11-21 18:14:33 -05:00
if ( styleNode . isAttr ( ) ) {
QString style = styleNode . toAttr ( ) . value ( ) . simplified ( ) . trimmed ( ) ;
style . replace ( " margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; " , " margin:0px 0px 0px 0px; " ) ;
style . replace ( " ; " , " ; " ) ;
2015-08-20 11:55:20 -04:00
if ( style = = " margin:0px 0px 0px 0px;-qt-block-indent:0;text-indent:0px; "
| | style . startsWith ( " -qt-paragraph-type:empty;margin:0px 0px 0px 0px;-qt-block-indent:0;text-indent:0px; " ) ) {
2012-01-30 19:06:24 -05:00
if ( addBR ) {
2015-08-20 11:55:20 -04:00
// add <br> after a removed <p> but not before a removed <p>
2012-01-30 19:06:24 -05:00
QDomElement elementBr = doc . createElement ( " br " ) ;
currentElement . insertBefore ( elementBr , element ) ;
+ + index ;
}
// remove Qt standard <p> or empty <p>
index + = element . childNodes ( ) . length ( ) ;
removeElement ( currentElement , element ) ;
2015-07-14 16:41:11 -04:00
// do not add extra <br> after empty paragraph, the paragraph already contains one
addBR = ! style . startsWith ( " -qt-paragraph-type:empty " ) ;
2012-01-30 19:06:24 -05:00
continue ;
2012-05-04 19:39:36 -04:00
}
2012-01-30 19:06:24 -05:00
}
addBR = false ;
}
2015-11-21 18:14:33 -05:00
// Compress style attribute
if ( styleNode . isAttr ( ) ) {
QDomAttr styleAttr = styleNode . toAttr ( ) ;
QString style = styleAttr . value ( ) . simplified ( ) . trimmed ( ) ;
style . replace ( " margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; " , " margin:0px 0px 0px 0px; " ) ;
style . replace ( " ; " , " ; " ) ;
QString className = knownStyle . value ( style ) ;
if ( className . isEmpty ( ) ) {
// Create a new class
className = QString ( " S%1 " ) . arg ( knownStyle . count ( ) ) ;
knownStyle . insert ( style , className ) ;
// Now add this for each attribute values
QStringList styles = style . split ( ' ; ' ) ;
foreach ( QString pair , styles ) {
pair . replace ( " " , " " ) ;
if ( ! pair . isEmpty ( ) ) {
QStringList * stylesListItem = stylesList . value ( pair ) ;
if ( ! stylesListItem ) {
// If value doesn't exist create it
stylesListItem = new QStringList ( ) ;
stylesList . insert ( pair , stylesListItem ) ;
}
//Add the new class to this value
stylesListItem - > push_back ( className ) ;
}
}
}
style . clear ( ) ;
node . attributes ( ) . removeNamedItem ( " style " ) ;
styleNode . clear ( ) ;
if ( ! className . isEmpty ( ) ) {
QDomNode classNode = doc . createAttribute ( " class " ) ;
classNode . setNodeValue ( className ) ;
node . attributes ( ) . setNamedItem ( classNode ) ;
}
}
2011-09-29 05:20:09 -04:00
}
+ + index ;
}
}
2015-08-20 11:55:20 -04:00
/**
* @ brief styleCreate : Add styles filtered in QDomDocument .
* @ param doc : QDomDocument containing all text formatted
* @ param stylesList : List of all styles recognized
* @ param flag : Bitfield of RSHTML_FORMATTEXT_ * constants ( they must be part of
* RSHTML_OPTIMIZEHTML_MASK ) .
* @ param bglum : Luminance background color of the widget where the text will be
* displayed . Needed only if RSHTML_FORMATTEXT_FIX_COLORS
* is passed inside flag .
* @ param desiredContrast : Minimum contrast between text and background color ,
* between 1 and 21.
2015-09-07 14:19:52 -04:00
* @ param desiredMinimumFontSize : Minimum font size .
2015-08-20 11:55:20 -04:00
*/
static void styleCreate ( QDomDocument & doc
, QHash < QString , QStringList * > stylesList
, unsigned int flag
, qreal bglum
2015-09-07 14:19:52 -04:00
, qreal desiredContrast
, int desiredMinimumFontSize )
2015-08-20 11:55:20 -04:00
{
QDomElement styleElem ;
do {
if ( doc . documentElement ( ) . namedItem ( " style " ) . toElement ( ) . attributeNode ( " RSOptimized " ) . isAttr ( ) ) {
QDomElement ele = doc . documentElement ( ) . namedItem ( " style " ) . toElement ( ) ;
//Remove child before filter
if ( ! ele . isElement ( ) ) break ; //Not an element so a bad message.
QDomAttr styleAttr = ele . attributeNode ( " RSOptimized " ) ;
if ( ! styleAttr . isAttr ( ) ) break ; //Not an attribute so a bad message.
QString version = styleAttr . value ( ) ;
if ( version = = " v2 " ) {
styleElem = ele ;
}
}
} while ( false ) ; //for break
if ( ! styleElem . isElement ( ) ) {
styleElem = doc . createElement ( " style " ) ;
// Creation of Style class list: <style type="text/css">
QDomAttr styleAttr ;
styleAttr = doc . createAttribute ( " type " ) ;
styleAttr . setValue ( " text/css " ) ;
styleElem . attributes ( ) . setNamedItem ( styleAttr ) ;
QDomAttr optAttr ;
optAttr = doc . createAttribute ( " RSOptimized " ) ;
optAttr . setValue ( " v2 " ) ;
styleElem . attributes ( ) . setNamedItem ( optAttr ) ;
}
while ( styleElem . childNodes ( ) . count ( ) > 0 ) {
styleElem . removeChild ( styleElem . firstChild ( ) ) ;
}
QString style = " " ;
QHashIterator < QString , QStringList * > it ( stylesList ) ;
while ( it . hasNext ( ) ) {
it . next ( ) ;
QStringList * classUsingIt = it . value ( ) ;
bool first = true ;
foreach ( QString className , * classUsingIt ) {
if ( ! className . trimmed ( ) . isEmpty ( ) ) {
style + = QString ( first ? " . " : " ,. " ) + className ; // + " ";
first = false ;
}
}
QStringList keyvalue = it . key ( ) . split ( ' : ' ) ;
if ( keyvalue . length ( ) = = 2 ) {
QString key = keyvalue . at ( 0 ) . trimmed ( ) ;
QString val = keyvalue . at ( 1 ) . trimmed ( ) ;
2015-09-07 14:19:52 -04:00
if ( key = = " font-size " ) {
QRegExp re ( " ( \\ d+) ( \ \ D * ) " ) ;
if ( re . indexIn ( val ) ! = - 1 ) {
bool ok ; int iVal = re . cap ( 1 ) . toInt ( & ok ) ;
if ( ok & & ( iVal < desiredMinimumFontSize ) ) {
val = QString : : number ( desiredMinimumFontSize ) + re . cap ( 2 ) ;
}
}
}
2015-08-20 11:55:20 -04:00
if ( ( flag & RSHTML_FORMATTEXT_REMOVE_FONT_FAMILY & & key = = " font-family " ) | |
( flag & RSHTML_FORMATTEXT_REMOVE_FONT_SIZE & & key = = " font-size " ) | |
( flag & RSHTML_FORMATTEXT_REMOVE_FONT_WEIGHT & & key = = " font-weight " ) | |
( flag & RSHTML_FORMATTEXT_REMOVE_FONT_STYLE & & key = = " font-style " ) ) {
continue ;
}
if ( flag & RSHTML_FORMATTEXT_REMOVE_COLOR ) {
if ( key = = " color " ) {
continue ;
}
} else if ( flag & RSHTML_FORMATTEXT_FIX_COLORS ) {
if ( key = = " color " ) {
findBestColor ( val , bglum , desiredContrast ) ;
}
}
if ( flag & ( RSHTML_FORMATTEXT_REMOVE_COLOR | RSHTML_FORMATTEXT_FIX_COLORS ) ) {
if ( key = = " background " | | key = = " background-color " ) {
// Remove background color because if we change the text color,
// it can become unreadable on the original background.
// Also, FIX_COLORS is intended to display text on the default
// background color of the operating system.
continue ;
}
}
//.S1 .S2 .S4 {font-family:'Sans';}
style + = " { " + key + " : " + val + " ;} " ;
} else {
style + = " { " + it . key ( ) + " ;} \n " ;
}
}
QDomText styleText = doc . createTextNode ( style ) ;
styleElem . appendChild ( styleText ) ;
//Create a Body element to be trunk, and doc could open it.
QDomElement trunk = doc . createElement ( " body " ) ;
trunk . appendChild ( styleElem ) ;
while ( ! doc . firstChild ( ) . isNull ( ) ) {
trunk . appendChild ( doc . firstChild ( ) . cloneNode ( ) ) ;
doc . removeChild ( doc . firstChild ( ) ) ;
}
doc . appendChild ( trunk ) ;
}
void RsHtml : : optimizeHtml ( QTextEdit * textEdit , QString & text , unsigned int flag /*= 0*/ )
2011-09-29 05:20:09 -04:00
{
if ( textEdit - > toHtml ( ) = = QTextDocument ( textEdit - > toPlainText ( ) ) . toHtml ( ) ) {
text = textEdit - > toPlainText ( ) ;
2013-03-19 17:45:13 -04:00
// std::cerr << "Optimized text to " << text.length() << " bytes , instead of " << textEdit->toHtml().length() << std::endl;
2011-09-29 05:20:09 -04:00
return ;
}
text = textEdit - > toHtml ( ) ;
2012-03-31 11:20:19 -04:00
optimizeHtml ( text , flag ) ;
2012-01-30 19:06:24 -05:00
}
2012-09-21 17:23:14 -04:00
/**
* @ brief Make an HTML document smaller by removing useless stuff .
* Can also change the text color to make it more readable .
* @ param [ in , out ] text HTML document .
* @ param flag Bitfield of RSHTML_FORMATTEXT_ * constants ( they must be part of
* RSHTML_OPTIMIZEHTML_MASK ) .
* @ param backgroundColor Background color of the widget where the text will be
* displayed . Needed only if RSHTML_FORMATTEXT_FIX_COLORS
* is passed inside flag .
* @ param desiredContrast Minimum contrast between text and background color ,
* between 1 and 21.
2015-09-07 14:19:52 -04:00
* @ param desiredMinimumFontSize Minimum font size .
2012-09-21 17:23:14 -04:00
*/
2015-08-20 11:55:20 -04:00
void RsHtml : : optimizeHtml ( QString & text , unsigned int flag /*= 0*/
2015-09-07 14:19:52 -04:00
, const QColor & backgroundColor /*= Qt::white*/
, qreal desiredContrast /*= 1.0*/
, int desiredMinimumFontSize /*=10*/
2015-08-20 11:55:20 -04:00
)
2012-01-30 19:06:24 -05:00
{
2011-09-29 05:20:09 -04:00
// remove doctype
text . remove ( QRegExp ( " <!DOCTYPE[^>]*> " ) ) ;
2014-01-18 17:55:14 -05:00
//remove all prepend char that make doc.setContent() fail
text . remove ( 0 , text . indexOf ( " < " ) ) ;
// Save Space and Tab because doc loose it.
text = saveSpace ( text ) ;
2011-09-29 05:20:09 -04:00
2014-01-18 17:55:14 -05:00
QString errorMsg ; int errorLine ; int errorColumn ;
2011-09-29 05:20:09 -04:00
QDomDocument doc ;
2014-01-18 17:55:14 -05:00
if ( doc . setContent ( text , & errorMsg , & errorLine , & errorColumn ) = = false ) {
2011-09-29 05:20:09 -04:00
return ;
}
QDomElement body = doc . documentElement ( ) ;
2015-08-20 11:55:20 -04:00
QHash < QString , QStringList * > stylesList ;
QHash < QString , QString > knownStyle ;
: : optimizeHtml ( doc , body , stylesList , knownStyle ) ;
2015-09-07 14:19:52 -04:00
: : styleCreate ( doc , stylesList , flag , : : getRelativeLuminance ( backgroundColor ) , desiredContrast , desiredMinimumFontSize ) ;
2011-09-29 05:20:09 -04:00
text = doc . toString ( - 1 ) ;
2013-03-19 17:45:13 -04:00
// std::cerr << "Optimized text to " << text.length() << " bytes , instead of " << originalLength << std::endl;
2011-09-29 05:20:09 -04:00
}
2012-05-11 20:40:53 -04:00
QString RsHtml : : toHtml ( QString text , bool realHtml )
2012-01-18 18:00:50 -05:00
{
// replace "\n" from the optimized html with "<br>"
text . replace ( " \n " , " <br> " ) ;
if ( ! realHtml ) {
return text ;
}
QTextDocument doc ;
doc . setHtml ( text ) ;
return doc . toHtml ( ) ;
}
2012-09-18 18:59:23 -04:00
/** Loads image and converts image to embedded image HTML fragment **/
bool RsHtml : : makeEmbeddedImage ( const QString & fileName , QString & embeddedImage , const int maxPixels )
{
QImage image ;
if ( image . load ( fileName ) = = false ) {
2015-03-27 14:41:44 -04:00
fprintf ( stderr , " RsHtml::makeEmbeddedImage() - image \" %s \" can't be load \n " , fileName . toLatin1 ( ) . constData ( ) ) ;
2012-09-18 18:59:23 -04:00
return false ;
}
return RsHtml : : makeEmbeddedImage ( image , embeddedImage , maxPixels ) ;
}
/** Converts image to embedded image HTML fragment **/
bool RsHtml : : makeEmbeddedImage ( const QImage & originalImage , QString & embeddedImage , const int maxPixels )
{
QByteArray bytearray ;
QBuffer buffer ( & bytearray ) ;
QImage resizedImage ;
const QImage * image = & originalImage ;
if ( maxPixels > 0 ) {
QSize imgSize = originalImage . size ( ) ;
if ( ( imgSize . height ( ) * imgSize . width ( ) ) > maxPixels ) {
// image is too large - resize keeping aspect ratio
QSize newSize ;
newSize . setWidth ( int ( qSqrt ( ( maxPixels * imgSize . width ( ) ) / imgSize . height ( ) ) ) ) ;
newSize . setHeight ( int ( ( imgSize . height ( ) * newSize . width ( ) ) / imgSize . width ( ) ) ) ;
// ask user
QMessageBox msgBox ;
2013-10-18 22:07:01 -04:00
msgBox . setText ( QString ( QApplication : : translate ( " RsHtml " , " Image is oversized for transmission. \n Reducing image to %1x%2 pixels? " ) ) . arg ( newSize . width ( ) ) . arg ( newSize . height ( ) ) ) ;
2012-09-18 18:59:23 -04:00
msgBox . setStandardButtons ( QMessageBox : : Ok | QMessageBox : : Cancel ) ;
msgBox . setDefaultButton ( QMessageBox : : Ok ) ;
if ( msgBox . exec ( ) ! = QMessageBox : : Ok ) {
return false ;
}
resizedImage = originalImage . scaled ( newSize , Qt : : KeepAspectRatio , Qt : : SmoothTransformation ) ;
image = & resizedImage ;
}
}
if ( buffer . open ( QIODevice : : WriteOnly ) ) {
if ( image - > save ( & buffer , " PNG " ) ) {
QByteArray encodedByteArray = bytearray . toBase64 ( ) ;
embeddedImage = " <img src= \" data:image/png;base64, " ;
embeddedImage . append ( encodedByteArray ) ;
embeddedImage . append ( " \" > " ) ;
} else {
2015-06-23 15:36:47 -04:00
//fprintf (stderr, "RsHtml::makeEmbeddedImage() - image can't be saved to buffer\n");
2012-09-18 18:59:23 -04:00
return false ;
}
} else {
2015-03-27 14:41:44 -04:00
fprintf ( stderr , " RsHtml::makeEmbeddedImage() - buffer can't be opened \n " ) ;
2012-09-18 18:59:23 -04:00
return false ;
}
return true ;
}
2013-07-01 16:35:07 -04:00
QString RsHtml : : plainText ( const QString & text )
{
2013-10-19 16:59:55 -04:00
# if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
return text . toHtmlEscaped ( ) ;
# else
2013-07-01 16:35:07 -04:00
return Qt : : escape ( text ) ;
2013-10-19 16:59:55 -04:00
# endif
2013-07-01 16:35:07 -04:00
}
QString RsHtml : : plainText ( const std : : string & text )
{
2013-10-19 16:59:55 -04:00
# if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
return QString : : fromUtf8 ( text . c_str ( ) ) . toHtmlEscaped ( ) ;
# else
2013-07-01 16:35:07 -04:00
return Qt : : escape ( QString : : fromUtf8 ( text . c_str ( ) ) ) ;
2013-10-19 16:59:55 -04:00
# endif
2013-07-01 16:35:07 -04:00
}
2015-11-19 06:05:52 -05:00
QString RsHtml : : makeQuotedText ( RSTextBrowser * browser )
{
QString text = browser - > textCursor ( ) . selection ( ) . toPlainText ( ) ;
if ( text . length ( ) = = 0 )
{
text = browser - > toPlainText ( ) ;
}
QStringList sl = text . split ( QRegExp ( " [ \r \n ] " ) , QString : : SkipEmptyParts ) ;
text = sl . join ( " \n > " ) ;
return QString ( " > " ) + text ;
}
2015-12-06 20:28:27 -05:00
void RsHtml : : insertSpoilerText ( QTextCursor cursor )
{
QString hiddentext = cursor . selection ( ) . toPlainText ( ) ;
if ( hiddentext . isEmpty ( ) ) return ;
QString publictext = " *SPOILER* " ;
QString encoded = hiddentext ;
encoded = encoded . replace ( QChar ( ' \" ' ) , QString ( " " " ) ) ;
encoded = encoded . replace ( QChar ( ' \' ' ) , QString ( " ' " ) ) ;
encoded = encoded . replace ( QChar ( ' < ' ) , QString ( " < " ) ) ;
encoded = encoded . replace ( QChar ( ' > ' ) , QString ( " > " ) ) ;
encoded = encoded . replace ( QChar ( ' & ' ) , QString ( " & " ) ) ;
QString html = QString ( " <a href= \" hidden:%1 \" title= \" %1 \" >%2</a> " ) . arg ( encoded , publictext ) ;
cursor . insertHtml ( html ) ;
}