Logo Search packages:      
Sourcecode: kmess version File versions  Download package

chatmessagestyle.cpp

/***************************************************************************
                          chatmessagestyle.cpp -  description
                             -------------------
    begin                : Sat Okt 29 2005
    copyright            : (C) 2005 by Diederik van der Boor
    email                : "vdboor" --at-- "codingdomain.com"
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/

#include "chatmessagestyle.h"

#include <qfont.h>
#include <qdatetime.h>
#include <qstylesheet.h>
#include <qregexp.h>
#include <qfile.h>

#include <klocale.h>
#include <kstddirs.h>
#include <kurl.h>

#include "../contact/contactbase.h"
#include "../emoticonmanager.h"
#include "../emoticon.h"
#include "../kmessdebug.h"
#include "../currentaccount.h"

#include "chatmessage.h"
#include "xsltransformation.h"



// The constructor
ChatMessageStyle::ChatMessageStyle()
  : QObject(0, "ChatMessageStyle")
  , contactFontColor_("#000000")
  , lastPendingEmoticonId_(0)
  , showTime_(true)
  , useContactFont_(false)
  , useEmoticons_(true)
  , useFontEffects_(true)
{
  xslTransformation_ = new XslTransformation();

  // Get placeholder for pending custom emoticons.
  pendingPlaceholder_ = KGlobal::dirs()->findResource("appdata", "pics/empty.png");
#ifdef KMESSTEST
  ASSERT( QFile::exists(pendingPlaceholder_) );
#endif
}



// The destructor
ChatMessageStyle::~ChatMessageStyle()
{
  delete xslTransformation_;
}



// Convert a chat message to HTML string
QString ChatMessageStyle::convertMessage(const ChatMessage &message)
{
  // Reset state, changed by parseMsnString
  pendingEmoticonTags_.clear();

  // Create and convert the XML message
  if(xslTransformation_ == 0 || ! xslTransformation_->hasStylesheet())
  {
#ifdef KMESSDEBUG_CHATVIEW
    kdWarning() << "ChatMessageStyle::convertMessage: no XSL theme loaded (theme=" << name_ << "),"
                << " using fallback theme." << endl;
#endif

    // Create fallback message.
    return createFallbackMessage(message);
  }
  else
  {
    QString xmlMessage    = createMessageXml(message);
    QString parsedMessage = xslTransformation_->convertXmlString(xmlMessage);

    // Strip DOCTYPE because it should only appear in convertMessageRoot() output
    parsedMessage = stripDoctype(parsedMessage);

    if( parsedMessage.isNull() )
    {
#ifdef KMESSDEBUG_CHATVIEW
      kdWarning() << "ChatMessageStyle::convertMessage: XSL conversion failed (theme=" << name_ << "),"
                  << " using fallback theme." << endl;
#endif
      return createFallbackMessage(message);
    }
    else if( isEmptyResult(parsedMessage) )
    {
#ifdef KMESSDEBUG_CHATVIEW
      kdWarning() << "ChatMessageStyle::convertMessage: XSL conversion returned no data (theme=" << name_ << "),"
                  << " using fallback theme." << endl;
#endif
      return createFallbackMessage(message);
    }
    else
    {
      // Received expected result
      return parsedMessage;  // DONE!
    }
  }
}



// Convert a group of chat message to HTML string
QString ChatMessageStyle::convertMessageList(const QPtrList<ChatMessage> &messageList)
{
  // Reset state, changed by parseMsnString
  pendingEmoticonTags_.clear();

  // Warn for empty list, avoid broken markup
  if( messageList.isEmpty() )
  {
    kdWarning() << "ChatMessageStyle::convertMessageList: no messages given!";
    return QString::null;
  }
  else if( messageList.count() == 1 )
  {
    // When there is only one message, don't wrap it in a <messagegroup>.
    // Makes the calling-code easier, and the layout consistent.
    return convertMessage( * messageList.getFirst() );
  }

  // Create iterator
  QPtrListIterator<ChatMessage> it(messageList);
  bool hasStyle = (xslTransformation_ != 0 && xslTransformation_->hasStylesheet());
  QString parsedMessage;

  // Avoid all the trouble when we can't create 
  if( ! hasStyle )
  {
#ifdef KMESSDEBUG_CHATVIEW
    kdWarning() << "ChatMessageStyle::convertMessageList: no XSL theme loaded (theme=" << name_ << "),"
                << " using fallback theme." << endl;
#endif
  }
  else
  {
    // Create XML string.
    QString xmlMessage = "<messagegroup>\n";
    while( it.current() != 0 )
    {
      xmlMessage += createMessageXml(*it.current()) + "\n";
      ++it;
    }
    xmlMessage += "</messagegroup>\n";

    // Convert
    parsedMessage = xslTransformation_->convertXmlString(xmlMessage);

    // Strip DOCTYPE because it should only appear in convertMessageRoot() output
    parsedMessage = stripDoctype(parsedMessage);

    if( parsedMessage.isNull() )
    {
#ifdef KMESSDEBUG_CHATVIEW
      kdWarning() << "ChatMessageStyle::convertMessageList: XSL conversion failed (theme=" << name_ << "),"
                  << " using fallback theme." << endl;
#endif
    }
    else if( isEmptyResult(parsedMessage) )
    {
#ifdef KMESSDEBUG_CHATVIEW
      kdWarning() << "ChatMessageStyle::convertMessage: XSL conversion returned no data (theme=" << name_ << "),"
                  << " using fallback theme." << endl;
#endif
    }
    else
    {
      // Received expected result
      return parsedMessage;  // DONE!
    }
  }

  // Conversion failed, create fallback
  while( it.current() != 0 )
  {
    parsedMessage += createFallbackMessage(*it.current());
    ++it;
  }
  return parsedMessage;
}



// Convert the message root.
QString ChatMessageStyle::convertMessageRoot()
{
  bool hasStyle = (xslTransformation_ != 0 && xslTransformation_->hasStylesheet());
  QString parsedMessage;

  // Avoid all the trouble when we can't create 
  if( ! hasStyle )
  {
#ifdef KMESSDEBUG_CHATVIEW
    kdWarning() << "ChatMessageStyle::convertMessageRoot: no XSL theme loaded." << endl;
#endif
  }
  else
  {
    // Not much to add here yet..
    QString xmlMessage = "<messageRoot>"
                         "</messageRoot>";

    // Convert
    parsedMessage = xslTransformation_->convertXmlString(xmlMessage);
    if( ! parsedMessage.isNull() )
    {
      if( isEmptyResult(parsedMessage) )
      {
        // Indicate the chat style doesn't define the whole header/footer,
        // so KMess can use it's default instead.
        return QString::null;
      }
      return parsedMessage;  // DONE!
    }
    else
    {
#ifdef KMESSDEBUG_CHATVIEW
      kdWarning() << "ChatMessageStyle::convertMessageRoot: XSL conversion failed (theme=" << name_ << ")." << endl;
#endif
    }
  }

  return QString::null;
}


// Convert the message as HTML, fallback method when XML fails.
QString ChatMessageStyle::createFallbackMessage(const ChatMessage &message)
{
  QString color;
  int     type    = message.getType();
  QString handle  = message.getContactHandle();
  QString name    = message.getContactName();
  QString body    = message.getBody();
  QString fontDir = body.isRightToLeft() ? "rtl" : "ltr";

  // Reset state, changed by parseMsnString
  pendingEmoticonTags_.clear();

  // Create the fallback message when the XML/XSL conversion failed
  if( message.isNormalMessage() )
  {
    // Contact message
    QString fontBefore;
    QString fontAfter;
    QString time;
    QFont   font;
    QString color;

    // Extract fonts
    parseFont(message, font, color, fontBefore, fontAfter);

    // Escape HTML, replace emoticons
    parseMsnString( name, handle /*, true */ );
    parseMsnString( body, handle );

    // Replace font special effects, like *bold*
    if( useFontEffects_ )
    {
      parseEffects( name );
      parseEffects( body );
    }

    // Replace newlines with <br/>
    parseBody(body);

    // Misc variables
    if( showTime_ )
    {
      QTime messageTime = message.getTime();
      time = KGlobal::locale()->formatTime( messageTime, true );
    }
    color = (type == ChatMessage::TYPE_INCOMING || type == ChatMessage::TYPE_OFFLINE_INCOMING)
            ? "666699" : "666666";

    // Create HTML
    return "<div dir='" + fontDir + "'><font color='" + color + "'>" + (showTime_ ? time + " " : "")
         + i18n("%1 says:").arg(name)
         + "<br></font>"
         + fontBefore + body + "<br><br>" + fontAfter
         + "</div>";
  }
  else if( type == ChatMessage::TYPE_SYSTEM )
  {
    // Red system message
    parseMsnString(body);
    parseBody(body);
    return "<div dir='" + fontDir + "'><hr size='2' color='red'>"
           "<font color='red'>" + body + "</font>"
           "<hr size='2' color='red'></div>";
  }
  else if( type == ChatMessage::TYPE_APPLICATION || type == ChatMessage::TYPE_APPLICATION_WEBCAM
        || type == ChatMessage::TYPE_APPLICATION_FILE || type == ChatMessage::TYPE_APPLICATION_AUDIO )
  {
    // Blue application message
    parseMsnString(body);
    parseBody(body);
    return "<div dir='" + fontDir + "'><font color='blue'>" + body + "</font><br></div>";
  }
  else
  {
    // Purple notification message 
    parseMsnString(body);
    parseBody(body);
    return "<div dir='" + fontDir + "'><font color='purple'>" + body + "</font><br></div>";
  }
}



// Convert the message as XML.
QString ChatMessageStyle::createMessageXml(const ChatMessage &message)
{
  QString date;
  QString time;
  QString fontBefore;
  QString fontAfter;
  QString xmlMessage;
  QString parsedMessage;
  QString typeString;
  QString color;
  QFont   font;

  // Get message info
  int     type      = message.getType();
  QString handle    = message.getContactHandle();
  QString name      = message.getContactName();
  QString body      = message.getBody();
  bool    isRtl     = body.isRightToLeft();
  bool    nameIsRtl = name.isRightToLeft(); 

  // See if this is a contact's message and not the user


  // Prepare the strings for HTML
  // App messages are not parsed, they may contain HTML (links to click on, etc..)
  if( message.isNormalMessage() )
  {
    parseFont(message, font, color, fontBefore, fontAfter);
    parseMsnString( name, handle /*, true */ );
    parseMsnString( body, handle );

    // Parse effects characters (_, *, /)
    if( useFontEffects_ )
    {
      parseEffects( name );
      parseEffects( body );
    }
  }


  // Replace newlines with <br/> tags
  parseBody(body);

  // Show the message time
  if( showTime_ )
  {
    // Get the time
    const QDateTime &messageDate = message.getDateTime();
    date = KGlobal::locale()->formatDate( messageDate.date(), true );
    time = KGlobal::locale()->formatTime( messageDate.time(), true );
  }


  // Get message type for XSL
  switch(type)
  {
    case ChatMessage::TYPE_INCOMING:         typeString = "incoming";        break;
    case ChatMessage::TYPE_OUTGOING:         typeString = "outgoing";        break;
    case ChatMessage::TYPE_SYSTEM:           typeString = "system";          break;
    case ChatMessage::TYPE_APPLICATION_FILE:
    case ChatMessage::TYPE_APPLICATION_WEBCAM:
    case ChatMessage::TYPE_APPLICATION_AUDIO:
    case ChatMessage::TYPE_APPLICATION:      typeString = "application";     break;
    case ChatMessage::TYPE_OFFLINE_INCOMING: typeString = "offlineIncoming"; break;
    case ChatMessage::TYPE_NOTIFICATION:
    default:
      typeString = "notification";
      break;
  }


  // IMPORTANT:
  // The QStyleSheet::escape() function doesn't escape quotes.
  // Special care needs to be taken to avoid XML parsing errors.

  // Create an XML message for the XSL theme conversion
  // "<?xml version='1.0'?>"
  xmlMessage = "<message"
               " type='" + escapeAttribute(typeString) + "'"
               + ( ! showTime_ ? QString::null : " date='" + escapeAttribute(date) + "'"
                                                 " time='" + escapeAttribute(time) + "'" ) +
               ">\n";

  // The contact can be empty for some notification messges
  if(! message.getContactHandle().isEmpty() )
  {
    xmlMessage += "  <from>\n"
                  "    <contact contactId='" + escapeAttribute( message.getContactHandle() ) + "'>\n";

   // If the user doesn't have a display picture, the style will use a default image.
    if( ! message.getContactPicturePath().isEmpty() && QFile::exists( message.getContactPicturePath() ) )
    {
      xmlMessage += "      <displayPicture url='" + escapeAttribute( message.getContactPicturePath() ) + "' />\n";
    }

    if( ! name.isEmpty() )
    {
      xmlMessage += "      <displayName text='" + escapeAttribute( name ) + "' dir='" + (nameIsRtl ? "rtl" : "ltr") + "' />\n";
    }

    xmlMessage += "    </contact>\n"
                  "  </from>\n";
  }

  // Add message
  xmlMessage += "  <body"
                " color='"         + escapeAttribute(color)         + "'"
                " fontFamily='"    + escapeAttribute(font.family()) + "'"
                " fontBold='"      + (font.bold()      ? "1" : "0") + "'"
                " fontItalic='"    + (font.italic()    ? "1" : "0") + "'"
                " fontUnderline='" + (font.underline() ? "1" : "0") + "'"
                " fontBefore='"    + escapeAttribute(fontBefore)    + "'"
                " fontAfter='"     + escapeAttribute(fontAfter)     + "'"
                " dir='"           + (isRtl ? "rtl" : "ltr")        + "'"
                ">" + QStyleSheet::escape( body ) + "</body>\n"  // escape again for XML
                "</message>\n";
  return xmlMessage;
}



// Escape the strings for use in XML attributes
QString ChatMessageStyle::escapeAttribute(const QString &value) const
{
  return QStyleSheet::escape(value).replace("\"", "&quot;").replace("'", "&apos;");
}



// Return the base folder of the style.
const QString & ChatMessageStyle::getBaseFolder() const
{
  return baseFolder_;
}



// Return the font used for contact messages, if forced to.
const QFont& ChatMessageStyle::getContactFont() const
{
  return contactFont_;
}



// Return the color of the forced contact font.
const QString& ChatMessageStyle::getContactFontColor() const
{
  return contactFontColor_;
}



// Return the css file attached to the stylesheet. Return null if there is none.
QString ChatMessageStyle::getCssFile() const
{
  QString cssFile = baseFolder_ + name_ + ".css";
  if( ! QFile::exists(cssFile) )
  {
    return QString::null;
  }
  else
  {
    return cssFile;
  }
}



// Return the name of the style.
const QString & ChatMessageStyle::getName() const
{
  return name_;
}



// Return the ID's of inserted <img> tags for the pending emoticons.
const QStringList & ChatMessageStyle::getPendingEmoticonTagIds() const
{
  return pendingEmoticonTags_;
}


// Return whether or not to show contact messages in the stored font.
bool ChatMessageStyle::getUseContactFont() const
{
  return useContactFont_;
}



// Return whether the given result is empty
bool ChatMessageStyle::isEmptyResult( const QString &parsedMessage )
{
  return stripDoctype(parsedMessage).stripWhiteSpace().isEmpty();
}



// Return whether an style is loaded.
bool ChatMessageStyle::hasStyle() const
{
  return (xslTransformation_ != 0 && xslTransformation_->hasStylesheet());
}



// Replace the newline characters
void ChatMessageStyle::parseBody(QString &body) const
{
  // Replace any newline characters in the message with "<br>" so that carriage returns will show properly.
  body = body.replace(QRegExp("\r?\n?$"), QString::null)  // Remove last \n
             .replace( "\r\n", "<br/>" )
             .replace( '\r',   "<br/>" )
             .replace( '\n',   "<br/>" );
}


// Do some effects characters (ie, bold, underline and italic specials)
void ChatMessageStyle::parseEffects(QString &text) const
{
  int       offset = 0;
  int       nextOffset = 0;
  QRegExp   effectsSearch;
  QString   effectsCharacter;
  QChar     boundaryCharacter;
  int       tagPosOpen, tagPosClose;
  QString   replacement;

  // Process bold, italics, underline
  const char* effectHtml   = "biu";  // bold, italic, underline
  QString effectCharacters = "*/_";

  for( int i = 0; i < 3; i++ )
  {
    effectsCharacter = QRegExp::escape( effectCharacters.mid(i, 1) );
    effectsSearch.setPattern( effectsCharacter + "([a-zA-Z0-9]+)" + effectsCharacter );

    offset = 0;

    while( offset >= 0 )
    {
      // Find next pattern match
      offset = effectsSearch.search( text, nextOffset );
      if( offset == -1 )
      {
        break;
      }

      // Skip effects contained in HTML tags
      tagPosOpen = text.find( "<", nextOffset );
      tagPosClose = text.find( ">", tagPosOpen );
      if( tagPosOpen != -1 && tagPosClose != -1 && offset > tagPosOpen && offset < tagPosClose )
      {
        // Continue from the first character after the HTML tag
        nextOffset = tagPosClose + 1;
        continue;
      }

      nextOffset = offset + effectsSearch.matchedLength();

      // Abort if there is a normal char before
      if( offset != 0 )
      {
        boundaryCharacter = text.at( offset - 1 );
        if( ! boundaryCharacter.isSpace() && ! boundaryCharacter.isPunct() )
        {
          continue;
        }
      }

      // Abort if there is a normal char after
      if( (uint) ( offset + effectsSearch.matchedLength() ) < text.length() )
      {
        boundaryCharacter = text.at( offset + effectsSearch.matchedLength() );
        if( ! boundaryCharacter.isSpace() && ! boundaryCharacter.isPunct() )
        {
          continue;
        }
      }

      // Replace if start and end are free.
      if( effectHtml[i] == 'b' )
      {
        // Bold is standard
        replacement = "*<b>" + effectsSearch.cap(1) + "</b>*";
      }
      else if( effectHtml[i] == 'i' )
      {
        // Same for italics
        replacement = "/<i>" + effectsSearch.cap(1) + "</i>/";
      }
      else if( effectHtml[i] == 'u' )
      {
        // For underline, re-use the <u> line, don't add an _ outside.
        replacement = "<u>&nbsp;" + effectsSearch.cap(1) + "&nbsp;</u>";
      }

      // Replace and change offset
      text.replace(offset, effectsSearch.matchedLength(), replacement);
      nextOffset = offset + replacement.length() + 2;
    }
  }
}



// Parse the font tags
void ChatMessageStyle::parseFont(const ChatMessage &message, QFont &font, QString &color, QString& fontBefore, QString& fontAfter) const
{
  // Extract the font from the message.
  ChatMessage::MessageType type = message.getType();
  if( useContactFont_ && (type == ChatMessage::TYPE_INCOMING || type == ChatMessage::TYPE_OFFLINE_INCOMING) )
  {
    // Replace the given font with the user's stored contact font
    font  = contactFont_;
    color = contactFontColor_;
  }
  else
  {
    // Use the font settings from the message
    font  = message.getFont();
    color = message.getFontColor();
  }

  // Include the dir="rtl" tag to the font so the text is at least displayed in the right direction.
  // To make it align right as well, the dir needs to be assigned to a block element.
  // This can't be done here as it would influence the chat style.
  QString fontDir = message.getBody().isRightToLeft() ? "rtl" : "ltr";

  // Create the font HTML for the message
  fontBefore = "";
  fontAfter  = "";
  if( font.bold()      ) fontBefore += "<b>";
  if( font.italic()    ) fontBefore += "<i>";
  if( font.underline() ) fontBefore += "<u>";
  fontBefore += "<font face=\"" + font.family() + "\" color=\"" + color + "\" dir=\"" + fontDir + "\">";
  fontAfter  += "</font>";
  if( font.underline() ) fontAfter  += "</u>";
  if( font.italic()    ) fontAfter  += "</i>";
  if( font.bold()      ) fontAfter  += "</b>";
}



// Replace the Messenger Plus characters with HTML markup
void ChatMessageStyle::parseMsnPlusString( QString &text ) const
{
  /*
    I'm really concerned about the way MSN+ embeds reserved characters into the
    text, which may cause i18n problems, but given the popularity of MSN+, this
    really needs to be implemented.
  */

  bool boldFlag      = false;
  bool italicFlag    = false;
  bool underlineFlag = false;
  bool fontFlag      = false;
  QColor  color;

  QRegExp htmlTest( "^\x04""&#?[a-z0-9]+;" );
  QRegExp fontCapture = QRegExp( "^\x03""([0-9]{1,2})(,([0-9]{1,2}))?" );

  for( unsigned int index = 0; index < text.length(); index++ )
  {
    switch( text.at( index ).unicode() )
    {
      case 0x0002: // bold character
        boldFlag = !boldFlag;

        text = text.replace( index, 1, ( boldFlag ) ? "<b>"  : "</b>" );
        index += ( boldFlag ) ? 2 : 3; // Skip the characters we've just added
        break;

      case 0x0003: // color character
        fontFlag = !fontFlag;

        fontCapture.search( text, index, QRegExp::CaretAtOffset );

        switch( fontCapture.cap(1).toInt() )
        {
          case 0:
            color = QColor( "white" );
            break;
          case 1:
            color = QColor( "black" );
            break;
          case 2:
            color = QColor( "blue" );
            break;
          case 3:
            color = QColor( "green" );
            break;
          case 4:
            color = QColor( "red" );
            break;
          case 5:
            color = QColor( "brown" );
            break;
          case 6:
            color = QColor( "purple" );
            break;
          case 7:
            color = QColor( "orange" );
            break;
          case 8:
            color = QColor( "yellow" );
            break;
          case 9:
            color = QColor( "lightGreen" );
            break;
          case 10:
            color = QColor( "cyan" );
            break;
          case 11:
            color = QColor( "lightBlue" );
            break;
          case 12:
            color = QColor( "blue" );
            break;
          case 13:
            color = QColor( "pink" );
            break;
          case 14:
            color = QColor( "darkGray" );
            break;
          case 15:
          default:
            color = QColor( "gray" );
            break;
        }

        // Font background text is ignored, as it's impossible to render in Qt's HTML subset.
        if( fontCapture.matchedLength() == -1 )
        {
          // No color found after the special character, close the html tag
          text = text.replace( index, 1, "</font>" );
          index += 6; // Skip the characters we've just added
        }
        else
        {
          // Font color open
          text = text.replace( index, fontCapture.matchedLength(), "<font color='" + color.name() + "'>" );
          index += 21; // Skip the characters we've just added
        }
        break;

      case 0x0004:
        // Sound tag: this character is followed by another which identifies the sound ID
        htmlTest.search( text, index, QRegExp::CaretAtOffset );
        if( htmlTest.matchedLength() != -1 )
        {
          // Some sounds IDs are HTML entities: that has to be taken care of, too.
          text = text.replace( index, htmlTest.matchedLength(), "" );
        }
        else
        {
          // we need to delete this character and the following one from the string
          text = text.replace( index, 2, "" );
        }

        // Restart from where we encountered the starting character
        index -= 1;
        break;

      case 0x0005: // italic character
        italicFlag = !italicFlag;

        text = text.replace( index, 1, ( italicFlag ) ? "<i>"  : "</i>" );
        index += ( italicFlag ) ? 2 : 3; // Skip the characters we've just added
        break;

      case 0x001f: // underline character
        underlineFlag = !underlineFlag;

        text = text.replace( index, 1, ( underlineFlag ) ? "<u>"  : "</u>" );
        index += ( underlineFlag ) ? 2 : 3; // Skip the characters we've just added
        break;
    }

  }

  // Close any tag still open. Hopefully, the parser will not complain too much if the closing order is wrong.
  if( boldFlag )  text.append( "</b>" );
  if( italicFlag )  text.append( "</i>" );
  if( underlineFlag )  text.append( "</u>" );
  if( fontFlag )  text.append( "</font>" );
}



// Replace the MSN characters with HTML markup
void ChatMessageStyle::parseMsnString( QString &text, const QString &handle, bool alwaysShowEmoticons )
{
  text.replace( "&", "&amp;" )
      .replace( "<", "&lt;" )
      .replace( ">", "&gt;" )
      .replace( "'", "&#39;" )
      .replace( '"', "&#34;" );

  // MessengerPlus is quite a "low level" encoding, so do it first
  parseMsnPlusString( text );

  // Links and emoticons are replaced in one loop cycle, traversing the message text.
  // Multiple search-replace cyles give unwanted side effects:
  // - smileys can pop up in links like ftp://user:pass@host/ and https://host
  // - emoticon replacements could be replaced by another cycle.


  bool                         allowAddingEmoticons = false;
  const EmoticonManager       *manager              = EmoticonManager::instance();

  // Build a collection of all emoticon data.
  const QRegExp               &emoticonRegExp       = manager->getHtmlPattern();
  const QMap<QString,QString> &emoticonReplacements = manager->getHtmlReplacements();


  QString code;
  QRegExp customRegExp;
  QRegExp pendingRegExp;
  QMap<QString,QString> customReplacements;
  QString customEmoticonsPattern;

  // Get theme of custom emoticons.
  if( ! handle.isEmpty() )
  {
    if( handle == CurrentAccount::instance()->getHandle() )
    {
        customRegExp       = manager->getHtmlPattern( true );
        customReplacements = manager->getHtmlReplacements( false, true );
        // We already have all of our emoticons, there are no pending ones
    }
    else
    {
      const ContactBase *contact = CurrentAccount::instance()->getContactByHandle( handle );
      if( contact != 0 )
      {
        customRegExp       = contact->getEmoticonPattern();
        customReplacements = contact->getEmoticonReplacements();
        pendingRegExp      = contact->getPendingEmoticonPattern();

        allowAddingEmoticons = true;
        customEmoticonsPattern =  manager->getHtmlPattern( true ).pattern();
      }
    }
  }


  QRegExp linkRegExp;
  linkRegExp.setPattern( "\\b(?:http://|https://|ftp://|sftp://|www\\.)"  // match protocol string
                         "[^ \r\n]+"                                      // followed by the host/path
                       );

  QRegExp emailRegExp;
  emailRegExp.setPattern(
                          "\\b("                // begin of word, start capture
                          "[a-z0-9_\\-\\.]+"    // match e-mail username
                          "\\@"                 // match '@'
                          "[a-z0-9\\-\\.]+"     // match domain hostname
                          "[a-z0-9]{2,3}"       // match top-level-domain
                          ")"                   // end capture`
                          "(?:[^a-zA-Z0-9\\-]|$)"  // not followed by more simple characters, or should find an end-of-line
                        );

  QRegExp geekLinkRegExp;
  geekLinkRegExp.setPattern(
                             "(^|\\b)"                // look-before test, for start of capture or word delimiter
                             "("                     // begin of word, start capture
                             "([a-z0-9\\-]+\\.)+"    // match simple characters, but it should contain a dot between each part.
                             "([a-z]{2,3})"          // finally match domain part 2 or 3 characters
                             ")"                     // end capture
                             "(?:[^a-zA-Z0-9]|$)"    // not followed by more simple characters, or should find an end-of-line
                           );

  QRegExp punctuationChars( "(?:[.,;!?\"'])$" );
  QRegExp invalidCcTld( "^(js|hh|cc|ui|fo|so|ko|qt|pp|cf|am|in|gz|ps|ai|rv|rm|wm)$" ); // block typical files instead of listing the whole country code list.
  QRegExp topLevelDomain( "^(?:com|org|net|edu|gov)$" );

  static const int REGEXP_COUNT = 6;
  const QRegExp* regexps[REGEXP_COUNT];
  regexps[0] = ( customRegExp.isEmpty()  ? 0 : &customRegExp );   // is first, to allow overwriting normal emoticons.
  regexps[1] = ( pendingRegExp.isEmpty() ? 0 : &pendingRegExp );
  regexps[2] = ( ( useEmoticons_ || alwaysShowEmoticons ) && ! emoticonRegExp.isEmpty() ? &emoticonRegExp : 0 );
  regexps[3] = &linkRegExp;
  regexps[4] = &emailRegExp;
  regexps[5] = &geekLinkRegExp;


#ifdef KMESSTEST
  ASSERT( emoticonRegExp.isValid() );
  ASSERT( emailRegExp.isValid() );
  ASSERT( linkRegExp.isValid() );
  ASSERT( geekLinkRegExp.isValid() );
#endif

  QString replacement;
  int lastPos = 0;
  int matches[ REGEXP_COUNT]; 
  memset( matches, -1, sizeof( matches ) );

  while(true)
  {
    // Find out which expression matches first.
    int matchedRegExp = -1;
    int matchStart    = -1;
    int matchedLength = 0;  // avoid gcc warning
    for(int i = 0; i < REGEXP_COUNT; i++)
    {
      if( regexps[ i ] == 0 )
      {
        continue;
      }

      matches[ i ] = regexps[ i ]->search( text, lastPos );
      if( matches[ i ] == -1 || (int) text.length() < matches[ i ] )
      {
        continue;
      }

#ifdef KMESSDEBUG_CHATVIEW
      kdDebug() << "ChatMessageStyle: regexp " << i << " matches at character " << matches[ i ] << endl;
#endif

      // See if it's before all other regexps
      if( matches[ i ] < matchStart || matchStart == -1 )
      {
        matchStart    = matches[ i ];
        matchedRegExp = i;
        matchedLength = regexps[ i ]->matchedLength();
      }
    }


    QString link;
    QString linkBefore;
    QString code;
    QString altText;
    QString placeholderId;

    // Determine the replacement for the matched expression.
    switch( matchedRegExp )
    {

      // Found a custom emoticon
      case 0:
        code = text.mid( matchStart, customRegExp.matchedLength() );  // cap(0) is not const.

        // Avoid replacing invalid emoticons with nothing
        if( ! customReplacements.contains( code ) )
        {
#ifdef KMESSDEBUG_CHATVIEW
          kdWarning() << "ChatMessageStyle::parseMsnString() - Emoticon replacement for '" << code << "' not found!" << endl;
#endif
          replacement = code;
          break;
        }

        replacement = customReplacements[ code ];

        // This emoticon is unknown, allow the user to add it by adding an internal KMess link to it
        if( allowAddingEmoticons && ! customEmoticonsPattern.contains( code ) )
        {
          QString urlCode = KURL::encode_string( code );

          // The name attribute is required as, if the user adds the emoticon, we'll want to make all links like this unclickable
          replacement = "<a name='newEmoticon_" + urlCode
                        + "' title='" + i18n( "Add this emoticon: %1" ).arg( code )
                        + "' href='kmess://emoticon/" + handle + "/" +  urlCode + "/" + KURL::encode_string( replacement )
                        + "'>"
                        + replacement
                        + "</a>";
        }
        break;


      // Found a custom emoticon, but the image file is still being downloaded.
      // Generate a placeholder tag, <img src="empty.png">, and update this tag later when the emoticon is received.
      case 1:
        // Generate and store placeholder ID.
        placeholderId = "ce" + QString::number( ++lastPendingEmoticonId_ );
        pendingEmoticonTags_.append( placeholderId );

        // Insert placeholder.
        code = text.mid( matchStart, pendingRegExp.matchedLength() );  // cap(0) is not const.
        replacement = "<img id='" + placeholderId
                    + "' src='" + QStyleSheet::escape( pendingPlaceholder_ )
                    + "' alt='" + code
                    + "' contact='" + QStyleSheet::escape( handle )
                    + "' width='16' height='16' valign='middle' class='customEmoticonPlaceholder' />";
        break;


      // Found an emoticon
      case 2:
        code = text.mid( matchStart, emoticonRegExp.matchedLength() );  // cap(0) is not const.
        if( emoticonReplacements.contains( code ) )
        {
          replacement = emoticonReplacements[ code ];
        }
        else
        {
          // HACK: Replace with the same string, to skip the entire code and continue parsing after it
          // See EmoticonTheme::updateCache()
          replacement = code;
#ifdef KMESSDEBUG_CHATVIEW
          kdDebug() << "ChatMessageStyle::parseMsnString() - Skipping unmatched code '" << code << "'" << endl;
#endif
        }

        break;


      // Found a link
      case 3:
        // When www. is found, automatically add http:// to the href.
        // This doesn't clash, because http:// links are matched earlier.
        link = linkRegExp.cap(0);
        if( ! link.isEmpty() )
        {
          // filter out puntuation char
          matchedLength = link.length();
          if( punctuationChars.match(link) != -1           // matches standard chars at end.
          ||  link.endsWith(")") && ! link.contains("(") )  // has ")" at end, unless it's a wikipedia disambiguation link
          {
            matchedLength--;
            link.remove( matchedLength, 1 );
          }

          // Create link
          replacement = ( link.startsWith("www.") )
                        ? replacement = "<a href=\"http://" + link + "\" target=\"_blank\">" + link + "</a>"
                        : replacement = "<a href=\""        + link + "\" target=\"_blank\">" + link + "</a>";
        }
        break;


      // Found a e-mail address.
      case 4:
        link = emailRegExp.cap(1);
        if( ! link.isEmpty() )
        {
          matchedLength = link.length();  // filter out puntuation char
          replacement   = "<a href=\"mailto:" + link + "\">" + link + "</a>";
        }
        break;


      // Found a geek-style link.
      case 5:
        linkBefore = geekLinkRegExp.cap(1); // matched look-before character.
        link       = geekLinkRegExp.cap(2);
        if( ! link.isEmpty() )
        {
          // Avoid matching "index.htm", "test.js" etc..
          // The list can never be complete but filter out 99% of the cases.
          QString tld = geekLinkRegExp.cap(4);
          if( ( tld.length() == 2 && invalidCcTld.match(tld)   == -1 )
          ||  ( tld.length() == 3 && topLevelDomain.match(tld) != -1 ) )
          {
            matchedLength = linkBefore.length() + link.length();  // filter out puntuation char
            replacement = linkBefore + "<a href=\"http://" + link + "/\" target=\"_blank\">" + link + "</a>";
          }
        }
        break;

      // Nothing was found, stop.
      case -1:
        break;

      default:
#ifdef KMESSTEST
        kdWarning() << "ChatMessageStyle: result of regular expression " << matchedRegExp << " is unhandled!" << endl;
#endif
        break;
    }

    // Nothing was found, stop.
    if( matchedRegExp == -1 )
    {
      // C doesn't allow us to break the while loop inside the switch statement, so break again.
      break;
    }

    // Process the replacement.
    if( replacement.isEmpty() || text.mid(matchStart, matchedLength).isEmpty() )
    {
      // No replacement found, move cursor to next char.
      lastPos = matchStart + 1;
    }
    else
    {
      // Replace the original text.
#ifdef KMESSDEBUG_CHATVIEW
      kdDebug() << "ChatMessageStyle: replacing '" << text.mid( matchStart, matchedLength ) << "' with: " << replacement << " (matched regexp=" << matchedRegExp << ")" << endl;
#endif
      text        = text.replace( matchStart, matchedLength, replacement );
      lastPos     = matchStart + replacement.length();
      replacement = QString::null;
    }
  }

  // Replace any "> "s in the message with ">&nbsp;" to avoid missing spaces after emoticons
  text = text.replace( "> ", ">&nbsp;" );

  // Replace double spaces with double &nbsp;s so that they'll show properly
  text = text.replace( "  ", "&nbsp;&nbsp;" );
}



// The the contact font
void ChatMessageStyle::setContactFont(const QFont &font)
{
  contactFont_ = font;
}



// The the contact font color
void ChatMessageStyle::setContactFontColor(const QString &fontColor)
{
  contactFontColor_ = fontColor;
}



// Set the show time state
void ChatMessageStyle::setShowTime(bool showTime)
{
  showTime_ = showTime;
}



// Set the message style, return false if it failed
bool ChatMessageStyle::setStyle(const QString &style)
{
  KStandardDirs *dirs   = KGlobal::dirs();
  QString path = dirs->findResource( "data", "kmess/styles/" + style + "/" + style + ".xsl" );

  if(path.isNull())
  {
    kdWarning() << "ChatMessageStyle::setStyle: could not find the style named '" << style << "'." << endl;
    return false;
  }

  // Set stylesheet
  name_ = style;
  xslTransformation_->setStylesheet(path);
  bool styleSheetLoaded = xslTransformation_->hasStylesheet();

  if(styleSheetLoaded)
  {
    // Update the base folder
    KURL pathUrl;
    pathUrl.setPath(path);
    baseFolder_ = pathUrl.directory(false);

    QMap<QString,QString> parameters;
    parameters["basepath"] = baseFolder_;
    parameters["csspath"]  = getCssFile();
    xslTransformation_->setParameters(parameters); 
  }

  // Indicate whether the stylesheet could be loaded
  return styleSheetLoaded;
}



// Enable or disable contact font overrides
void ChatMessageStyle::setUseContactFont(bool useContactFont)
{
  useContactFont_ = useContactFont;
}



// Enable or disable emoticons
void ChatMessageStyle::setUseEmoticons(bool useEmoticons)
{
  useEmoticons_ = useEmoticons;
}



// Enable or disable font effects
void ChatMessageStyle::setUseFontEffects(bool useFontEffects)
{
  useFontEffects_ = useFontEffects;
}



// Strip the DOCTYPE tag from the message
QString ChatMessageStyle::stripDoctype( const QString &parsedMessage )
{
  if( parsedMessage.startsWith( "<!DOCTYPE" ) )
  {
    QRegExp re(">\r?\n?");
    int endPos = parsedMessage.find(re);
    if( endPos == -1 )
    {
      kdWarning() << "ChatMesageStyle: Could not strip DOCTYPE tag: end position not found!" << endl;
      return parsedMessage;
    }

    // Strip both end end possible \r\n character
    return parsedMessage.mid( endPos + re.matchedLength() );
  }
  else
  {
    return parsedMessage;
  }
}



#include "chatmessagestyle.moc"


Generated by  Doxygen 1.6.0   Back to index