You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tdenetwork/kopete/libkopete/private/kopeteemoticons.cpp

562 lines
15 KiB

/*
kopeteemoticons.cpp - Kopete Preferences Container-Class
Copyright (c) 2002 by Stefan Gehn <metz AT gehn.net>
Copyright (c) 2002-2006 by Olivier Goffart <ogoffart @ kde.org>
Copyright (c) 2005 by Engin AYDOGAN <engin@bzzzt.biz>
Kopete (c) 2002-2005 by the Kopete developers <kopete-devel@kde.org>
*************************************************************************
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Lesser General Public *
* License as published by the Free Software Foundation; either *
* version 2 of the License, or (at your option) any later version. *
* *
*************************************************************************
*/
#include "kopeteemoticons.h"
#include "kopeteprefs.h"
#include <tqdom.h>
#include <tqfile.h>
#include <tqstylesheet.h>
#include <tqimage.h>
#include <tqdatetime.h>
#include <tdeapplication.h>
#include <kdebug.h>
#include <kstandarddirs.h>
#include <tdeversion.h>
#include <set>
#include <algorithm>
#include <iterator>
/*
* Testcases can be found in the kopeteemoticontest app in the tests/ directory.
*/
namespace Kopete {
struct Emoticons::Emoticon
{
Emoticon(){}
/* sort by longest to shortest matchText */
bool operator< (const Emoticon &e){ return matchText.length() > e.matchText.length(); }
TQString matchText;
TQString matchTextEscaped;
TQString picPath;
TQString picHTMLCode;
};
/* This is the object we will store each emoticon match in */
struct Emoticons::EmoticonNode {
const Emoticon emoticon;
int pos;
EmoticonNode() : emoticon(), pos( -1 ) {}
EmoticonNode( const Emoticon e, int p ) : emoticon( e ), pos( p ) {}
};
class Emoticons::Private
{
public:
TQMap<TQChar, TQValueList<Emoticon> > emoticonMap;
TQMap<TQString, TQStringList> emoticonAndPicList;
/**
* The current icon theme from KopetePrefs
*/
TQString theme;
};
Emoticons *Emoticons::s_self = 0L;
Emoticons *Emoticons::self()
{
if( !s_self )
s_self = new Emoticons;
return s_self;
}
TQString Emoticons::parseEmoticons(const TQString& message, ParseMode mode ) //static
{
return self()->parse( message, mode );
}
TQValueList<Emoticons::Token> Emoticons::tokenizeEmoticons( const TQString& message, ParseMode mode ) // static
{
return self()->tokenize( message, mode );
}
TQValueList<Emoticons::Token> Emoticons::tokenize( const TQString& message, uint mode )
{
TQValueList<Token> result;
if ( !KopetePrefs::prefs()->useEmoticons() )
{
result.append( Token( Text, message ) );
return result;
}
if( ! ( mode & (StrictParse|RelaxedParse) ) )
{
//if none of theses two mode are selected, use the mode from the config
mode |= KopetePrefs::prefs()->emoticonsRequireSpaces() ? StrictParse : RelaxedParse ;
}
/* previous char, in the firs iteration assume that it is space since we want
* to let emoticons at the beginning, the very first previous TQChar must be a space. */
TQChar p = ' ';
TQChar c; /* current char */
TQChar n; /* next character after a match candidate, if strict this should be TQChar::null or space */
/* This is the EmoticonNode container, it will represent each matched emoticon */
TQValueList<EmoticonNode> foundEmoticons;
TQValueList<EmoticonNode>::const_iterator found;
/* First-pass, store the matched emoticon locations in foundEmoticons */
TQValueList<Emoticon> emoticonList;
TQValueList<Emoticon>::const_iterator it;
size_t pos;
bool inHTMLTag = false;
bool inHTMLLink = false;
bool inHTMLEntity = false;
TQString needle; // search for this
for ( pos = 0; pos < message.length(); pos++ )
{
c = message[ pos ];
if ( mode & SkipHTML ) // Shall we skip HTML ?
{
if ( !inHTMLTag ) // Are we already in an HTML tag ?
{
if ( c == '<' ) { // If not check if are going into one
inHTMLTag = true; // If we are, change the state to inHTML
p = c;
continue;
}
}
else // We are already in a HTML tag
{
if ( c == '>' ) { // Check if it ends
inHTMLTag = false; // If so, change the state
if ( p == 'a' )
{
inHTMLLink = false;
}
}
else if ( c == 'a' && p == '<' ) // check if we just entered an achor tag
{
inHTMLLink = true; // don't put smileys in urls
}
p = c;
continue;
}
if( !inHTMLEntity )
{ // are we
if( c == '&' )
{
inHTMLEntity = true;
}
}
}
if ( inHTMLLink ) // i can't think of any situation where a link adress might need emoticons
{
p = c;
continue;
}
if ( (mode & StrictParse) && !p.isSpace() && p != '>')
{ // '>' may mark the end of an html tag
p = c;
continue;
} /* strict requires space before the emoticon */
if ( d->emoticonMap.contains( c ) )
{
emoticonList = d->emoticonMap[ c ];
bool found = false;
for ( it = emoticonList.begin(); it != emoticonList.end(); ++it )
{
// If this is an HTML, then search for the HTML form of the emoticon.
// For instance <o) => &gt;o)
needle = ( mode & SkipHTML ) ? (*it).matchTextEscaped : (*it).matchText;
if ( ( pos == (size_t)message.find( needle, pos ) ) )
{
if( mode & StrictParse )
{
/* check if the character after this match is space or end of string*/
n = message[ pos + needle.length() ];
//<br/> marks the end of a line
if( n != '<' && !n.isSpace() && !n.isNull() && n!= '&')
break;
}
/* Perfect match */
foundEmoticons.append( EmoticonNode( (*it), pos ) );
found = true;
/* Skip the matched emoticon's matchText */
pos += needle.length() - 1;
break;
}
}
if( !found )
{
if( inHTMLEntity ){
// If we are in an HTML entitiy such as &gt;
int htmlEnd = message.find( ';', pos );
// Search for where it ends
if( htmlEnd == -1 )
{
// Apparently this HTML entity isn't ended, something is wrong, try skip the '&'
// and continue
kdDebug( 14000 ) << k_funcinfo << "Broken HTML entity, trying to recover." << endl;
inHTMLEntity = false;
pos++;
}
else
{
pos = htmlEnd;
inHTMLEntity = false;
}
}
}
} /* else no emoticons begin with this character, so don't do anything */
p = c;
}
/* if no emoticons found just return the text */
if ( foundEmoticons.isEmpty() )
{
result.append( Token( Text, message ) );
return result;
}
/* Second-pass, generate tokens based on the matches */
pos = 0;
int length;
for ( found = foundEmoticons.begin(); found != foundEmoticons.end(); ++found )
{
needle = ( mode & SkipHTML ) ? (*found).emoticon.matchTextEscaped : (*found).emoticon.matchText;
if ( ( length = ( (*found).pos - pos ) ) )
{
result.append( Token( Text, message.mid( pos, length ) ) );
result.append( Token( Image, (*found).emoticon.matchTextEscaped, (*found).emoticon.picPath, (*found).emoticon.picHTMLCode ) );
pos += length + needle.length();
}
else
{
result.append( Token( Image, (*found).emoticon.matchTextEscaped, (*found).emoticon.picPath, (*found).emoticon.picHTMLCode ) );
pos += needle.length();
}
}
if ( message.length() - pos ) // if there is remaining regular text
{
result.append( Token( Text, message.mid( pos ) ) );
}
return result;
}
Emoticons::Emoticons( const TQString &theme ) : TQObject( kapp, "KopeteEmoticons" )
{
// kdDebug(14010) << "KopeteEmoticons::KopeteEmoticons" << endl;
d=new Private;
if(theme.isNull())
{
initEmoticons();
connect( KopetePrefs::prefs(), TQ_SIGNAL(saved()), this, TQ_SLOT(initEmoticons()) );
}
else
{
initEmoticons( theme );
}
}
Emoticons::~Emoticons( )
{
delete d;
}
void Emoticons::addIfPossible( const TQString& filenameNoExt, const TQStringList &emoticons )
{
TDEStandardDirs *dir = TDEGlobal::dirs();
TQString pic;
//maybe an extension was given, so try to find the exact file
pic = dir->findResource( "emoticons", d->theme + TQString::fromLatin1( "/" ) + filenameNoExt );
if( pic.isNull() )
pic = dir->findResource( "emoticons", d->theme + TQString::fromLatin1( "/" ) + filenameNoExt + TQString::fromLatin1( ".mng" ) );
if ( pic.isNull() )
pic = dir->findResource( "emoticons", d->theme + TQString::fromLatin1( "/" ) + filenameNoExt + TQString::fromLatin1( ".png" ) );
if ( pic.isNull() )
pic = dir->findResource( "emoticons", d->theme + TQString::fromLatin1( "/" ) + filenameNoExt + TQString::fromLatin1( ".gif" ) );
if( !pic.isNull() ) // only add if we found one file
{
TQPixmap p;
TQString result;
d->emoticonAndPicList.insert( pic, emoticons );
for ( TQStringList::const_iterator it = emoticons.constBegin(), end = emoticons.constEnd();
it != end; ++it )
{
TQString matchEscaped=TQStyleSheet::escape(*it);
Emoticon e;
e.picPath = pic;
// We need to include size (width, height attributes) hints in the emoticon HTML code
// Unless we do so, ChatMessagePart::slotScrollView does not work properly and causing
// HTMLPart not to be scrolled to the very last message.
p.load( e.picPath );
result = TQString::fromLatin1( "<img align=\"center\" src=\"" ) +
e.picPath +
TQString::fromLatin1( "\" title=\"" ) +
matchEscaped +
TQString::fromLatin1( "\" width=\"" ) +
TQString::number( p.width() ) +
TQString::fromLatin1( "\" height=\"" ) +
TQString::number( p.height() ) +
TQString::fromLatin1( "\" />" );
e.picHTMLCode = result;
e.matchTextEscaped = matchEscaped;
e.matchText = *it;
d->emoticonMap[ matchEscaped[0] ].append( e );
d->emoticonMap[ (*it)[0] ].append( e );
}
}
}
void Emoticons::initEmoticons( const TQString &theme )
{
if(theme.isNull())
{
if ( d->theme == KopetePrefs::prefs()->iconTheme() )
return;
d->theme = KopetePrefs::prefs()->iconTheme();
}
else
{
d->theme = theme;
}
// kdDebug(14010) << k_funcinfo << "Called" << endl;
d->emoticonAndPicList.clear();
d->emoticonMap.clear();
TQString filename= TDEGlobal::dirs()->findResource( "emoticons", d->theme + TQString::fromLatin1( "/emoticons.xml" ) );
if(!filename.isEmpty())
return initEmoticon_emoticonsxml( filename );
filename= TDEGlobal::dirs()->findResource( "emoticons", d->theme + TQString::fromLatin1( "/icondef.xml" ) );
if(!filename.isEmpty())
return initEmoticon_JEP0038( filename );
kdWarning(14010) << k_funcinfo << "emotiucon XML theme description not found" <<endl;
}
void Emoticons::initEmoticon_emoticonsxml( const TQString & filename)
{
TQDomDocument emoticonMap( TQString::fromLatin1( "messaging-emoticon-map" ) );
TQFile mapFile( filename );
mapFile.open( IO_ReadOnly );
emoticonMap.setContent( &mapFile );
TQDomElement list = emoticonMap.documentElement();
TQDomNode node = list.firstChild();
while( !node.isNull() )
{
TQDomElement element = node.toElement();
if( !element.isNull() )
{
if( element.tagName() == TQString::fromLatin1( "emoticon" ) )
{
TQString emoticon_file = element.attribute(
TQString::fromLatin1( "file" ), TQString() );
TQStringList items;
TQDomNode emoticonNode = node.firstChild();
while( !emoticonNode.isNull() )
{
TQDomElement emoticonElement = emoticonNode.toElement();
if( !emoticonElement.isNull() )
{
if( emoticonElement.tagName() == TQString::fromLatin1( "string" ) )
{
items << emoticonElement.text();
}
else
{
kdDebug(14010) << k_funcinfo <<
"Warning: Unknown element '" << element.tagName() <<
"' in emoticon data" << endl;
}
}
emoticonNode = emoticonNode.nextSibling();
}
addIfPossible ( emoticon_file, items );
}
else
{
kdDebug(14010) << k_funcinfo << "Warning: Unknown element '" <<
element.tagName() << "' in map file" << endl;
}
}
node = node.nextSibling();
}
mapFile.close();
sortEmoticons();
}
void Emoticons::initEmoticon_JEP0038( const TQString & filename)
{
TQDomDocument emoticonMap( TQString::fromLatin1( "icondef" ) );
TQFile mapFile( filename );
mapFile.open( IO_ReadOnly );
emoticonMap.setContent( &mapFile );
TQDomElement list = emoticonMap.documentElement();
TQDomNode node = list.firstChild();
while( !node.isNull() )
{
TQDomElement element = node.toElement();
if( !element.isNull() )
{
if( element.tagName() == TQString::fromLatin1( "icon" ) )
{
TQStringList items;
TQString emoticon_file;
TQDomNode emoticonNode = node.firstChild();
while( !emoticonNode.isNull() )
{
TQDomElement emoticonElement = emoticonNode.toElement();
if( !emoticonElement.isNull() )
{
if( emoticonElement.tagName() == TQString::fromLatin1( "text" ) )
{
//TODO xml:lang
items << emoticonElement.text();
}
else if( emoticonElement.tagName() == TQString::fromLatin1( "object" ) && emoticon_file.isEmpty() )
{
TQString mime= emoticonElement.attribute(
TQString::fromLatin1( "mime" ), TQString::fromLatin1("image/*") );
if(mime.startsWith(TQString::fromLatin1("image/")) && !mime.endsWith(TQString::fromLatin1("/svg+xml")))
{
emoticon_file = emoticonElement.text();
}
else
{
kdDebug(14010) << k_funcinfo << "Warning: Unsupported format '" << mime << endl;
}
}
/*else
{
kdDebug(14010) << k_funcinfo <<
"Warning: Unknown element '" << element.tagName() <<
"' in emoticon data" << endl;
}*/
}
emoticonNode = emoticonNode.nextSibling();
}
if( !items.isEmpty() && !emoticon_file.isEmpty() )
addIfPossible ( emoticon_file, items );
}
else
{
kdDebug(14010) << k_funcinfo << "Warning: Unknown element '" <<
element.tagName() << "' in map file" << endl;
}
}
node = node.nextSibling();
}
mapFile.close();
sortEmoticons();
}
void Emoticons::sortEmoticons()
{
/* sort strings in order of longest to shortest to provide convenient input for
greedy matching in the tokenizer */
TQValueList<TQChar> keys = d->emoticonMap.keys();
for ( TQValueList<TQChar>::const_iterator it = keys.begin(); it != keys.end(); ++it )
{
TQChar key = (*it);
TQValueList<Emoticon> keyValues = d->emoticonMap[key];
qHeapSort(keyValues.begin(), keyValues.end());
d->emoticonMap[key] = keyValues;
}
}
TQMap<TQString, TQStringList> Emoticons::emoticonAndPicList()
{
return d->emoticonAndPicList;
}
TQString Emoticons::parse( const TQString &message, ParseMode mode )
{
if ( !KopetePrefs::prefs()->useEmoticons() )
return message;
TQValueList<Token> tokens = tokenize( message, mode );
TQValueList<Token>::const_iterator token;
TQString result;
TQPixmap p;
for ( token = tokens.begin(); token != tokens.end(); ++token )
{
switch ( (*token).type )
{
case Text:
result += (*token).text;
break;
case Image:
result += (*token).picHTMLCode;
kdDebug( 14010 ) << k_funcinfo << "Emoticon html code: " << result << endl;
break;
default:
kdDebug( 14010 ) << k_funcinfo << "Unknown token type. Something's broken." << endl;
}
}
return result;
}
void Emoticons::reload()
{
d->emoticonAndPicList.clear();
d->emoticonMap.clear();
initEmoticons( KopetePrefs::prefs()->iconTheme() );
}
} //END namesapce Kopete
#include "kopeteemoticons.moc"