Logo Search packages:      
Sourcecode: kmess version File versions

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 "../emoticoncollection.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, 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, using fallback theme." << endl;
#endif
      return createFallbackMessage(message);
    }
    else if( isEmptyResult(parsedMessage) )
    {
#ifdef KMESSDEBUG_CHATVIEW
      kdWarning() << "ChatMessageStyle::convertMessage: XSL conversion returned no data, 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, 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, using fallback theme." << endl;
#endif
    }
    else if( isEmptyResult(parsedMessage) )
    {
#ifdef KMESSDEBUG_CHATVIEW
      kdWarning() << "ChatMessageStyle::convertMessage: XSL conversion returned no data, 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." << 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 )
  {
    // 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:      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() ) + "'";
    if( ! message.getContactPicturePath().isEmpty() )
    {
      xmlMessage += " userPhotoUrl='" + escapeAttribute( message.getContactPicturePath() ) + "'";
    }
    xmlMessage += ">\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;
  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;
      }

      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 fontCapture = QRegExp( "(,{0,1})([0-9]{1,2})" );

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

        text = text.replace( index, 1, ( boldFlag ) ? "<b>"  : "</b>" );

        break;
      case 0x0003:
        fontFlag = !fontFlag;

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

        switch( fontCapture.cap(2).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:
            color = QColor( "gray" );
        }

        if ( fontCapture.cap(1) == "," )
        {
          // font background text
        }
        else
        {
          text = text.replace( index, fontCapture.matchedLength() + 1, "<font color='" + color.name() + "'>" );
        }

        break;
      case 0x0005:
        italicFlag = !italicFlag;

        text = text.replace( index, 1, ( italicFlag ) ? "<i>"  : "</i>" );

        break;
      case 0x001f:
        underlineFlag = !underlineFlag;

        text = text.replace( index, 1, ( italicFlag ) ? "<u>"  : "</u>" );

        break;
    }
  }
}



// Replace the MSN characters with HTML markup
void ChatMessageStyle::parseMsnString( QString &text, const QString &handle, bool alwaysShowEmoticons )
{
  // MessengerPlus is quite a "low level" encoding, so do it first
  parseMsnPlusString( text );

  text = text.replace( "&", "&amp;" )
             .replace( "<", "&lt;" )
             .replace( ">", "&gt;" );

  // Make sure ::) works by replacing :: with &3A:
  while ( text.contains( "::" ) )
  {
    text = text.replace( "::", "&3A:" );
  }

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

  // Build a collection of all emoticon data.
  const QRegExp               &emoticonRegExp       = EmoticonCollection::instance()->getPattern();
  const QMap<QString,QString> &emoticonReplacements = EmoticonCollection::instance()->getReplacements();

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

  // Get collection of custom emoticons.
  if( ! handle.isEmpty() )
  {
    const ContactBase *contact = CurrentAccount::instance()->getContactByHandle(handle);
    if( contact != 0 )
    {
      customRegExp       = contact->getEmoticonPattern();
      customReplacements = contact->getEmoticonReplacements();
      pendingRegExp      = contact->getPendingEmoticonPattern();
    }
  }

  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(
                             "(^|[:;, ])"            // look-before test, only allow some characters
                             "("                     // 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 : 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 )
      {
        matches[i] = regexps[i]->search(text, lastPos);
        if( matches[i] != -1 )
        {
#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();
          }
        }
      }
    }

    // Determine the replacement for the matched expression.
    if( matchedRegExp == -1 )
    {
      // Nothing found, stop.
      break;
    }
    else if( matchedRegExp == 0 )
    {
      // Found a custom emoticon
      code = text.mid(matchStart, customRegExp.matchedLength());  // cap(0) is not const.
      if( customReplacements.contains(code) )
      {
        replacement = customReplacements[code];
      }
    }
    else if( matchedRegExp == 1 )
    {
      // 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.

      // Generate and store placeholder ID.
      QString 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='" + QStyleSheet::escape(code) + "' contact='" + QStyleSheet::escape(handle)
                  + "' width='16' height='16' valign='middle' title='' class='customEmoticonPlaceholder' />";
    }
    else if( matchedRegExp == 2 )
    {
      // Found a emoticon
      QString code = text.mid(matchStart, emoticonRegExp.matchedLength());  // cap(0) is not const.
      if( emoticonReplacements.contains(code) )
      {
        replacement = emoticonReplacements[code];
      }
    }
    else if( matchedRegExp == 3 )
    {
      // Found a link
      // When www. is found, automatically add http:// to the href.
      // This doesn't clash, because http:// links are matched earlier.
      QString 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>";
      }
    }
    else if( matchedRegExp == 4 )
    {
      // Found a e-mail address.
      QString link = emailRegExp.cap(1);
      if( ! link.isEmpty() )
      {
        matchedLength = link.length();  // filter out puntuation char
        replacement = "<a href=\"mailto:" + link + "\">" + link + "</a>";
      }
    }
    else if( matchedRegExp == 5 )
    {
      // Found a geek-style link.
      QString linkBefore = geekLinkRegExp.cap(1); // matched look-before character.
      QString 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>";
        }
      }
    }
#ifdef KMESSTEST
    else
    {
      kdWarning() << "ChatMessageStyle: result of regular expression " << matchedRegExp << " is unhandled!" << endl;
    }
#endif

    // Process the replacement.
    if( replacement.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;
    }
  }

  // Return the &3As to :s.
  text = text.replace( "&3A", ":" );

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