| | 1 | /*************************************************************************** |
| | 2 | msnplus.cpp - adds MSN Plus! codes/features |
| | 3 | ------------------- |
| | 4 | begin : April 30, 2008 |
| | 5 | copyright : (C) 2008 by Valerio Pilo |
| | 6 | email : valerio@kmess.org |
| | 7 | ***************************************************************************/ |
| | 8 | |
| | 9 | /*************************************************************************** |
| | 10 | * * |
| | 11 | * This program is free software; you can redistribute it and/or modify * |
| | 12 | * it under the terms of the GNU General Public License as published by * |
| | 13 | * the Free Software Foundation; either version 2 of the License, or * |
| | 14 | * (at your option) any later version. * |
| | 15 | * * |
| | 16 | ***************************************************************************/ |
| | 17 | |
| | 18 | #include "msnplus.h" |
| | 19 | |
| | 20 | #include <math.h> |
| | 21 | |
| | 22 | #include <QColor> |
| | 23 | #include <QRegExp> |
| | 24 | #include <QString> |
| | 25 | #include <QTextDocument> |
| | 26 | |
| | 27 | #include "../kmessdebug.h" |
| | 28 | |
| | 29 | |
| | 30 | /// KILLME @todo put in kmessdebug.h |
| | 31 | #define KMESSDEBUG_MSNPLUS |
| | 32 | |
| | 33 | |
| | 34 | |
| | 35 | // Return the given string with MSN Plus! formatting stripped out |
| | 36 | QString MsnPlus::getCleanString( const QString &string ) |
| | 37 | { |
| | 38 | static QHash<QString, QString> cleanedStringsCache; |
| | 39 | // Check if the string is already in cache |
| | 40 | if( cleanedStringsCache.contains( string ) ) |
| | 41 | { |
| | 42 | return cleanedStringsCache.value( string ); |
| | 43 | } |
| | 44 | |
| | 45 | // Make a changeable copy of the given string |
| | 46 | QString parsed = string; |
| | 47 | |
| | 48 | // First check if the string does not need modification |
| | 49 | if( ! parsed.contains( "[" ) /*&& ! parsed.contains( "\xB7" )*/ ) |
| | 50 | { |
| | 51 | return string; |
| | 52 | } |
| | 53 | |
| | 54 | // The output string will contain HTML, extra care has to be taken not to fool the parsers with false positives |
| | 55 | if( Qt::mightBeRichText( parsed ) ) |
| | 56 | { |
| | 57 | parsed.replace( "&", "&" ).replace( "<", "<" ).replace( ">", ">" ); |
| | 58 | } |
| | 59 | |
| | 60 | parsed.replace( "[b]", "" ) |
| | 61 | .replace( "[/b]", "" ) |
| | 62 | .replace( "[i]", "" ) |
| | 63 | .replace( "[/i]", "" ) |
| | 64 | .replace( "[u]", "" ) |
| | 65 | .replace( "[/u]", "" ) |
| | 66 | .replace( "[s]", "" ) |
| | 67 | .replace( "[/s]", "" ); |
| | 68 | |
| | 69 | parsed.replace( QRegExp( "\\[/?c=?#?[0-9a-z,]*\\]", Qt::CaseInsensitive ), "" ) |
| | 70 | .replace( QRegExp( "\\[/?a=?#?[0-9a-z,]*\\]", Qt::CaseInsensitive ), "" ); |
| | 71 | |
| | 72 | #ifdef KMESSDEBUG_MSNPLUS |
| | 73 | kDebug() << "Original:" << string; |
| | 74 | kDebug() << "Parsed:" << parsed; |
| | 75 | #endif |
| | 76 | |
| | 77 | // Add this to the cache |
| | 78 | cleanedStringsCache.insert( string, parsed ); |
| | 79 | |
| | 80 | // Keep the queue size to the maximum allowed length |
| | 81 | if( cleanedStringsCache.count() > MSN_PLUS_STRINGCACHESIZE ) |
| | 82 | { |
| | 83 | cleanedStringsCache.remove( cleanedStringsCache.constBegin().key() ); |
| | 84 | } |
| | 85 | |
| | 86 | return parsed; |
| | 87 | } |
| | 88 | |
| | 89 | |
| | 90 | |
| | 91 | // Return the given string with MSN Plus! formatting parsed |
| | 92 | QString MsnPlus::getFormattedString( const QString &string ) |
| | 93 | { |
| | 94 | static QHash<QString, QString> formattedStringsCache; |
| | 95 | |
| | 96 | // Check if the string is already in cache |
| | 97 | if( formattedStringsCache.contains( string ) ) |
| | 98 | { |
| | 99 | return formattedStringsCache.value( string ); |
| | 100 | } |
| | 101 | |
| | 102 | // Make a changeable copy of the given string |
| | 103 | QString parsed = string; |
| | 104 | |
| | 105 | // First check if the string does not need modification |
| | 106 | if( ! parsed.contains( "[" ) /*&& ! parsed.contains( "\xB7" )*/ ) |
| | 107 | { |
| | 108 | return string; |
| | 109 | } |
| | 110 | |
| | 111 | // The output string will contain HTML, extra care has to be taken not to fool the parsers with false positives |
| | 112 | if( Qt::mightBeRichText( parsed ) ) |
| | 113 | { |
| | 114 | parsed.replace( "&", "&" ).replace( "<", "<" ).replace( ">", ">" ); |
| | 115 | } |
| | 116 | |
| | 117 | parsed.replace( "[b]", "<b>" ) |
| | 118 | .replace( "[/b]", "</b>" ) |
| | 119 | .replace( "[i]", "<i>" ) |
| | 120 | .replace( "[/i]", "</i>" ) |
| | 121 | .replace( "[u]", "<u>" ) |
| | 122 | .replace( "[/u]", "</u>" ) |
| | 123 | .replace( "[s]", "<s>" ) |
| | 124 | .replace( "[/s]", "</s>" ); |
| | 125 | |
| | 126 | QRegExp colorMatch( "\\[(c|a)=(#?[0-9a-z]+)\\](.*)\\[/\\1(?:=(#?[0-9a-z]+))?\\]", Qt::CaseInsensitive ); |
| | 127 | |
| | 128 | while( colorMatch.indexIn( parsed ) != -1 ) |
| | 129 | { |
| | 130 | bool isForeground = ( colorMatch.cap( 1 ) == "c" ); |
| | 131 | |
| | 132 | // match a solid color |
| | 133 | if( colorMatch.cap( 4 ).isEmpty() ) |
| | 134 | { |
| | 135 | parsed.replace( colorMatch.pos(), colorMatch.matchedLength(), |
| | 136 | "<span style='" + QString( isForeground ? "color" : "background-color" ) + ":" + getHtmlColor( colorMatch.cap( 2 ) ) + ";'>" + |
| | 137 | colorMatch.cap( 3 ) + "</span>" ); |
| | 138 | } |
| | 139 | // Match a foreground color gradient |
| | 140 | else if( isForeground ) |
| | 141 | { |
| | 142 | parsed.replace( colorMatch.pos(), colorMatch.matchedLength(), |
| | 143 | getHtmlGradient( colorMatch.cap( 3 ), colorMatch.cap( 2 ), colorMatch.cap( 4 ) ) ); |
| | 144 | } |
| | 145 | // Match a background color gradient |
| | 146 | else |
| | 147 | { |
| | 148 | parsed.replace( colorMatch.pos(), colorMatch.matchedLength(), |
| | 149 | "<span style='background-color:qlineargradient(x1:0,y1:0,x2:1,y2:0," |
| | 150 | "stop:0 " + getHtmlColor( colorMatch.cap( 2 ) ) + ",stop:1 " + getHtmlColor( colorMatch.cap( 4 ) ) + ");'>" + |
| | 151 | colorMatch.cap( 3 ) + "</span>" ); |
| | 152 | } |
| | 153 | } |
| | 154 | //color: ; |
| | 155 | #ifdef KMESSDEBUG_MSNPLUS |
| | 156 | kDebug() << "Original:" << string; |
| | 157 | kDebug() << "Parsed:" << parsed; |
| | 158 | #endif |
| | 159 | |
| | 160 | // Add the parsed string in a tag which is not usually used elsewhere: this drastically reduces parsing problems |
| | 161 | // originated by, for example, missing closing tags. |
| | 162 | parsed = "<font>" + parsed + "</font>"; |
| | 163 | |
| | 164 | // Add this to the cache |
| | 165 | formattedStringsCache.insert( string, parsed ); |
| | 166 | |
| | 167 | // Keep the queue size to the maximum allowed length |
| | 168 | if( formattedStringsCache.count() > MSN_PLUS_STRINGCACHESIZE ) |
| | 169 | { |
| | 170 | formattedStringsCache.remove( formattedStringsCache.constBegin().key() ); |
| | 171 | } |
| | 172 | |
| | 173 | return parsed; |
| | 174 | } |
| | 175 | |
| | 176 | |
| | 177 | |
| | 178 | // Turns color codes (english color names, RGB triplets, MSN Plus! palette colors) into an HTML RGB color code |
| | 179 | QString MsnPlus::getHtmlColor( const QString& color ) |
| | 180 | { |
| | 181 | // Find arbitrary RGB triplets |
| | 182 | if( color.contains( "," ) ) |
| | 183 | { |
| | 184 | QStringList rgb = color.split( ",", QString::KeepEmptyParts ); |
| | 185 | QColor rgbColor( rgb[0].toInt(), rgb[1].toInt(), rgb[2].toInt() ); |
| | 186 | |
| | 187 | if( rgbColor.isValid() ) |
| | 188 | { |
| | 189 | return rgbColor.name(); |
| | 190 | } |
| | 191 | } |
| | 192 | |
| | 193 | // Find HTML (#RRGGBB/#RGB) or CSS (red,blue) color codes |
| | 194 | QColor cssOrHtmlColor( color ); |
| | 195 | if( cssOrHtmlColor.isValid() ) |
| | 196 | { |
| | 197 | return cssOrHtmlColor.name(); |
| | 198 | } |
| | 199 | |
| | 200 | // Find colors in the MSN Plus! palette. Note that a whole lot of colors are still missing from the list. |
| | 201 | switch( color.toInt() ) |
| | 202 | { |
| | 203 | case MSN_PLUS_COLOR_WHITE: return "#FFFFFF"; |
| | 204 | case MSN_PLUS_COLOR_MARINE: return "#000066"; |
| | 205 | case MSN_PLUS_COLOR_GREEN: return "#009900"; |
| | 206 | case MSN_PLUS_COLOR_RED: return "#FF0000"; |
| | 207 | case MSN_PLUS_COLOR_BROWN: return "#660000"; |
| | 208 | case MSN_PLUS_COLOR_PURPLE: return "#990099"; |
| | 209 | case MSN_PLUS_COLOR_ORANGE: return "#FF6600"; |
| | 210 | case MSN_PLUS_COLOR_YELLOW: return "#FFFF00"; |
| | 211 | case MSN_PLUS_COLOR_LIME: return "#00FF00"; |
| | 212 | case MSN_PLUS_COLOR_TEAL: return "#006699"; |
| | 213 | case MSN_PLUS_COLOR_AQUA: return "#00FFFF"; |
| | 214 | case MSN_PLUS_COLOR_BLUE: return "#0000FF"; |
| | 215 | case MSN_PLUS_COLOR_PINK: return "#FF00FF"; |
| | 216 | case MSN_PLUS_COLOR_GRAY: return "#666666"; |
| | 217 | case MSN_PLUS_COLOR_SILVER: return "#CCCCCC"; |
| | 218 | case MSN_PLUS_COLOR_BLACK: |
| | 219 | default: return "#000000"; |
| | 220 | } |
| | 221 | } |
| | 222 | |
| | 223 | |
| | 224 | |
| | 225 | // Turns a string into a gradient colored one, using Qt HTML tags |
| | 226 | QString MsnPlus::getHtmlGradient( const QString& text, const QString& startColor, const QString& endColor ) |
| | 227 | { |
| | 228 | QColor start( getHtmlColor( startColor ) ); |
| | 229 | QColor end ( getHtmlColor( endColor ) ); |
| | 230 | |
| | 231 | if( ! start.isValid() || ! end.isValid() || text.isEmpty() ) |
| | 232 | { |
| | 233 | return text; |
| | 234 | } |
| | 235 | |
| | 236 | bool inTag = false, inEntity = false; |
| | 237 | QColor current = start; |
| | 238 | QChar character; |
| | 239 | QString outputText = ""; |
| | 240 | int indexFullString; |
| | 241 | unsigned int levels, indexGradient = 0; |
| | 242 | int differenceRed, differenceGreen, differenceBlue, tempRed, tempGreen, tempBlue; |
| | 243 | |
| | 244 | // Get the length of the string which has to be converted, stripped from HTML tags (which will not be |
| | 245 | // colored) and from HTML entities, which don't count towards the gradient levels, since are output |
| | 246 | // as a whole with a single color |
| | 247 | levels = QString( text ).replace( QRegExp( "<[^>]+>|&[a-z]+;" ), "" ).length(); |
| | 248 | |
| | 249 | // Calculate the RGB difference between the starting and ending color |
| | 250 | differenceRed = (int)floor( ( start.red () - end.red () ) / (float)levels ); |
| | 251 | differenceGreen = (int)floor( ( start.green() - end.green() ) / (float)levels ); |
| | 252 | differenceBlue = (int)floor( ( start.blue () - end.blue () ) / (float)levels ); |
| | 253 | |
| | 254 | #ifdef KMESSDEBUG_MSNPLUS |
| | 255 | kDebug() << "Size is " << text.length() << " (" << levels << " stripped) - " |
| | 256 | << "Colored from " << start.name() << " to " << end.name() << ", difference: (" |
| | 257 | << differenceRed << "," << differenceGreen << "," << differenceBlue << ")." << endl; |
| | 258 | #endif |
| | 259 | |
| | 260 | // Proceed through the entire original string |
| | 261 | for( indexFullString = 0; indexFullString < text.length(); indexFullString++ ) |
| | 262 | { |
| | 263 | character = text[ indexFullString ]; |
| | 264 | |
| | 265 | // Match HTML tags: they must be skipped entirely |
| | 266 | if( character == '<' || character == '>' ) |
| | 267 | { |
| | 268 | inTag = ( character == '<' ); |
| | 269 | outputText += character; |
| | 270 | continue; |
| | 271 | } |
| | 272 | // Match HTML entities: they must be enclosed as a whole in a single font |
| | 273 | else if( character == '&' || character == ';' ) |
| | 274 | { |
| | 275 | if( character == '&' ) |
| | 276 | { |
| | 277 | inEntity = true; |
| | 278 | outputText += "<span style=\"color:" + current.name() + ";\">" + character; |
| | 279 | } |
| | 280 | else |
| | 281 | { |
| | 282 | inEntity = false; |
| | 283 | outputText += character; |
| | 284 | outputText += "</span>"; |
| | 285 | } |
| | 286 | indexGradient++; |
| | 287 | continue; |
| | 288 | } |
| | 289 | |
| | 290 | // Characters into tags or entities are output directly, and no gradient is calculated. |
| | 291 | if( inTag || inEntity ) |
| | 292 | { |
| | 293 | outputText += character; |
| | 294 | continue; |
| | 295 | } |
| | 296 | |
| | 297 | // Get the new values for the current gradient character |
| | 298 | tempRed = start.red () - ( differenceRed * indexGradient ); |
| | 299 | tempGreen = start.green() - ( differenceGreen * indexGradient ); |
| | 300 | tempBlue = start.blue () - ( differenceBlue * indexGradient ); |
| | 301 | |
| | 302 | // The values may get out of the limits, and since setRgb() voids the whole RGB color if one of the values is |
| | 303 | // out of range, we must assure them to be always in range. |
| | 304 | current.setRgb( tempRed < 0 ? 0 : ( tempRed > 255 ? 255 : tempRed ), |
| | 305 | tempGreen < 0 ? 0 : ( tempGreen > 255 ? 255 : tempGreen ), |
| | 306 | tempBlue < 0 ? 0 : ( tempBlue > 255 ? 255 : tempBlue ) ); |
| | 307 | |
| | 308 | // kdDebug() << indexGradient << " -> " << character << ": " << current.name() << endl; |
| | 309 | |
| | 310 | // Use the <font> to save characters |
| | 311 | outputText += "<font color='" + current.name() + "'>" + character + "</font>"; |
| | 312 | |
| | 313 | indexGradient++; |
| | 314 | } |
| | 315 | |
| | 316 | return outputText; |
| | 317 | } |
| | 318 | |
| | 319 | |