/** * linklocator.cpp * * Copyright (c) 2002 Dave Corrie * * This file is part of KMail. * * KMail is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "linklocator.h" #include "pimemoticons.h" #include #include #include #include #include #include #include #include #include TQMap *LinkLocator::s_smileyEmoticonNameMap = 0; TQMap *LinkLocator::s_smileyEmoticonHTMLCache = 0; static KStaticDeleter< TQMap > smileyMapDeleter; static KStaticDeleter< TQMap > smileyCacheDeleter; LinkLocator::LinkLocator(const TQString& text, int pos) : mText(text), mPos(pos), mMaxUrlLen(4096), mMaxAddressLen(255) { // If you change either of the above values for maxUrlLen or // maxAddressLen, then please also update the documentation for // setMaxUrlLen()/setMaxAddressLen() in the header file AND the // default values used for the maxUrlLen/maxAddressLen parameters // of convertToHtml(). if ( !s_smileyEmoticonNameMap ) { smileyMapDeleter.setObject( s_smileyEmoticonNameMap, new TQMap() ); for ( int i = 0; i < EmotIcons::EnumSindex::COUNT; ++i ) { TQString imageName( EmotIcons::EnumSindex::enumToString[i] ); imageName.truncate( imageName.length() - 2 ); //remove the _0 bit s_smileyEmoticonNameMap->insert( EmotIcons::smiley(i), imageName ); } } if ( !s_smileyEmoticonHTMLCache ) smileyCacheDeleter.setObject( s_smileyEmoticonHTMLCache, new TQMap() ); } void LinkLocator::setMaxUrlLen(int length) { mMaxUrlLen = length; } int LinkLocator::maxUrlLen() const { return mMaxUrlLen; } void LinkLocator::setMaxAddressLen(int length) { mMaxAddressLen = length; } int LinkLocator::maxAddressLen() const { return mMaxAddressLen; } TQString LinkLocator::getUrl() { TQString url; if(atUrl()) { // handle cases like this: http://foobar.org/ int start = mPos; while(mPos < (int)mText.length() && mText[mPos] > ' ' && mText[mPos] != '"' && TQString("<>()[]").find(mText[mPos]) == -1) { ++mPos; } /* some URLs really end with: # / & - _ */ const TQString allowedSpecialChars = TQString("#/&-_"); while(mPos > start && mText[mPos-1].isPunct() && allowedSpecialChars.find(mText[mPos-1]) == -1 ) { --mPos; } url = mText.mid(start, mPos - start); if(isEmptyUrl(url) || mPos - start > maxUrlLen()) { mPos = start; url = ""; } else { --mPos; } } return url; } // keep this in sync with KMMainWin::slotUrlClicked() bool LinkLocator::atUrl() const { // the following characters are allowed in a dot-atom (RFC 2822): // a-z A-Z 0-9 . ! # $ % & ' * + - / = ? ^ _ ` { | } ~ const TQString allowedSpecialChars = TQString(".!#$%&'*+-/=?^_`{|}~"); // the character directly before the URL must not be a letter, a number or // any other character allowed in a dot-atom (RFC 2822). if( ( mPos > 0 ) && ( mText[mPos-1].isLetterOrNumber() || ( allowedSpecialChars.find( mText[mPos-1] ) != -1 ) ) ) return false; TQChar ch = mText[mPos]; return (ch=='h' && ( mText.mid(mPos, 7) == "http://" || mText.mid(mPos, 8) == "https://") ) || (ch=='v' && mText.mid(mPos, 6) == "vnc://") || (ch=='f' && ( mText.mid(mPos, 7) == "fish://" || mText.mid(mPos, 6) == "ftp://" || mText.mid(mPos, 7) == "ftps://") ) || (ch=='s' && ( mText.mid(mPos, 7) == "sftp://" || mText.mid(mPos, 6) == "smb://") ) || (ch=='m' && mText.mid(mPos, 7) == "mailto:") || (ch=='w' && mText.mid(mPos, 4) == "www.") || (ch=='f' && mText.mid(mPos, 4) == "ftp.") || (ch=='n' && mText.mid(mPos, 5) == "news:"); // note: no "file:" for security reasons } bool LinkLocator::isEmptyUrl(const TQString& url) { return url.isEmpty() || url == "http://" || url == "https://" || url == "fish://" || url == "ftp://" || url == "ftps://" || url == "sftp://" || url == "smb://" || url == "vnc://" || url == "mailto" || url == "www" || url == "ftp" || url == "news" || url == "news://"; } TQString LinkLocator::getEmailAddress() { TQString address; if ( mText[mPos] == '@' ) { // the following characters are allowed in a dot-atom (RFC 2822): // a-z A-Z 0-9 . ! # $ % & ' * + - / = ? ^ _ ` { | } ~ const TQString allowedSpecialChars = TQString(".!#$%&'*+-/=?^_`{|}~"); // determine the local part of the email address int start = mPos - 1; while ( start >= 0 && mText[start].unicode() < 128 && ( mText[start].isLetterOrNumber() || mText[start] == '@' || // allow @ to find invalid email addresses allowedSpecialChars.find( mText[start] ) != -1 ) ) { if ( mText[start] == '@' ) return TQString(); // local part contains '@' -> no email address --start; } ++start; // we assume that an email address starts with a letter or a digit while ( ( start < mPos ) && !mText[start].isLetterOrNumber() ) ++start; if ( start == mPos ) return TQString(); // local part is empty -> no email address // determine the domain part of the email address int dotPos = INT_MAX; int end = mPos + 1; while ( end < (int)mText.length() && ( mText[end].isLetterOrNumber() || mText[end] == '@' || // allow @ to find invalid email addresses mText[end] == '.' || mText[end] == '-' ) ) { if ( mText[end] == '@' ) return TQString(); // domain part contains '@' -> no email address if ( mText[end] == '.' ) dotPos = TQMIN( dotPos, end ); // remember index of first dot in domain ++end; } // we assume that an email address ends with a letter or a digit while ( ( end > mPos ) && !mText[end - 1].isLetterOrNumber() ) --end; if ( end == mPos ) return TQString(); // domain part is empty -> no email address if ( dotPos >= end ) return TQString(); // domain part doesn't contain a dot if ( end - start > maxAddressLen() ) return TQString(); // too long -> most likely no email address address = mText.mid( start, end - start ); mPos = end - 1; } return address; } TQString LinkLocator::convertToHtml(const TQString& plainText, int flags, int maxUrlLen, int maxAddressLen) { LinkLocator locator(plainText); locator.setMaxUrlLen(maxUrlLen); locator.setMaxAddressLen(maxAddressLen); TQString str; TQString result((TQChar*)0, (int)locator.mText.length() * 2); TQChar ch; int x; bool startOfLine = true; TQString emoticon; for (locator.mPos = 0, x = 0; locator.mPos < (int)locator.mText.length(); locator.mPos++, x++) { ch = locator.mText[locator.mPos]; if ( flags & PreserveSpaces ) { if (ch==' ') { if (startOfLine) { result += " "; locator.mPos++, x++; startOfLine = false; } while (locator.mText[locator.mPos] == ' ') { result += " "; locator.mPos++, x++; if (locator.mText[locator.mPos] == ' ') { result += " "; locator.mPos++, x++; } } locator.mPos--, x--; continue; } else if (ch=='\t') { do { result += " "; x++; } while((x&7) != 0); x--; startOfLine = false; continue; } } if (ch=='\n') { result += "
"; startOfLine = true; x = -1; continue; } startOfLine = false; if (ch=='&') result += "&"; else if (ch=='"') result += """; else if (ch=='<') result += "<"; else if (ch=='>') result += ">"; else { const int start = locator.mPos; if ( !(flags & IgnoreUrls) ) { str = locator.getUrl(); if (!str.isEmpty()) { TQString hyperlink; if(str.left(4) == "www.") hyperlink = "http://" + str; else if(str.left(4) == "ftp.") hyperlink = "ftp://" + str; else hyperlink = str; str = str.replace('&', "&"); result += "" + str + ""; x += locator.mPos - start; continue; } str = locator.getEmailAddress(); if(!str.isEmpty()) { // len is the length of the local part int len = str.find('@'); TQString localPart = str.left(len); // remove the local part from the result (as '&'s have been expanded to // & we have to take care of the 4 additional characters per '&') result.truncate(result.length() - len - (localPart.contains('&')*4)); x -= len; result += "" + str + ""; x += str.length() - 1; continue; } } if ( flags & ReplaceSmileys ) { str = locator.getEmoticon(); if ( ! str.isEmpty() ) { result += str; x += locator.mPos - start; continue; } } if ( flags & HighlightText ) { str = locator.highlightedText(); if ( !str.isEmpty() ) { result += str; x += locator.mPos - start; continue; } } result += ch; } } return result; } TQString LinkLocator::pngToDataUrl( const TQString & iconPath ) { if ( iconPath.isEmpty() ) return TQString(); TQFile pngFile( iconPath ); if ( !pngFile.open( IO_ReadOnly | IO_Raw ) ) return TQString(); TQByteArray ba = pngFile.readAll(); pngFile.close(); return TQString::fromLatin1("data:image/png;base64,%1") .arg( KCodecs::base64Encode( ba ).data() ); } TQString LinkLocator::getEmoticon() { // smileys have to be prepended by whitespace if ( ( mPos > 0 ) && !mText[mPos-1].isSpace() ) return TQString(); // since smileys start with ':', ';', '(' or '8' short circuit method const TQChar ch = mText[mPos]; if ( ch !=':' && ch != ';' && ch != '(' && ch != '8' ) return TQString(); // find the end of the smiley (a smiley is at most 4 chars long and ends at // lineend or whitespace) const int MinSmileyLen = 2; const int MaxSmileyLen = 4; int smileyLen = 1; while ( ( smileyLen <= MaxSmileyLen ) && ( mPos+smileyLen < (int)mText.length() ) && !mText[mPos+smileyLen].isSpace() ) smileyLen++; if ( smileyLen < MinSmileyLen || smileyLen > MaxSmileyLen ) return TQString(); const TQString smiley = mText.mid( mPos, smileyLen ); if ( !s_smileyEmoticonNameMap->contains( smiley ) ) return TQString(); // that's not a (known) smiley TQString htmlRep; if ( s_smileyEmoticonHTMLCache->contains( smiley ) ) { htmlRep = (*s_smileyEmoticonHTMLCache)[smiley]; } else { const TQString imageName = (*s_smileyEmoticonNameMap)[smiley]; #if KDE_IS_VERSION( 3, 3, 91 ) const TQString iconPath = locate( "emoticons", EmotIcons::theme() + TQString::fromLatin1( "/" ) + imageName + TQString::fromLatin1(".png") ); #else const TQString iconPath = locate( "data", TQString::fromLatin1( "kopete/pics/emoticons/" )+ EmotIcons::theme() + TQString::fromLatin1( "/" ) + imageName + TQString::fromLatin1(".png") ); #endif const TQString dataUrl = pngToDataUrl( iconPath ); if ( dataUrl.isEmpty() ) { htmlRep = TQString(); } else { // create an image tag (the text in attribute alt is used // for copy & paste) representing the smiley htmlRep = TQString("") .arg( dataUrl, TQStyleSheet::escape( smiley ), TQStyleSheet::escape( smiley ) ); } s_smileyEmoticonHTMLCache->insert( smiley, htmlRep ); } if ( !htmlRep.isEmpty() ) mPos += smileyLen - 1; return htmlRep; } TQString LinkLocator::highlightedText() { // formating symbols must be prepended with a whitespace if ( ( mPos > 0 ) && !mText[mPos-1].isSpace() ) return TQString(); const TQChar ch = mText[mPos]; if ( ch != '/' && ch != '*' && ch != '_' ) return TQString(); TQRegExp re = TQRegExp( TQString("\\%1([0-9A-Za-z]+)\\%2").arg( ch ).arg( ch ) ); if ( re.search( mText, mPos ) == mPos ) { uint length = re.matchedLength(); // there must be a whitespace after the closing formating symbol if ( mPos + length < mText.length() && !mText[mPos + length].isSpace() ) return TQString(); mPos += length - 1; switch ( ch.latin1() ) { case '*': return "" + re.cap( 1 ) + ""; case '_': return "" + re.cap( 1 ) + ""; case '/': return "" + re.cap( 1 ) + ""; } } return TQString(); }