| | 157 | /* |
|---|
| | 158 | * @brief Perform a single emoticon replacement |
|---|
| | 159 | * |
|---|
| | 160 | * This method may call itself recursively to perform additional adjacent emoticon replacements |
|---|
| | 161 | * |
|---|
| | 162 | * @result The location in messageString of the beginning of the emoticon replaced, or NSNotFound if no replacement was made |
|---|
| | 163 | */ |
|---|
| | 164 | - (unsigned int)replaceAnEmoticonStartingAtLocation:(unsigned *)currentLocation |
|---|
| | 165 | fromString:(NSString *)messageString |
|---|
| | 166 | originalAttributedString:(NSAttributedString *)originalAttributedString |
|---|
| | 167 | intoString:(NSMutableAttributedString **)newMessage |
|---|
| | 168 | replacementCount:(unsigned *)replacementCount |
|---|
| | 169 | callingRecursively:(BOOL)callingRecursively |
|---|
| | 170 | context:(id)serviceClassContext |
|---|
| | 171 | { |
|---|
| | 172 | unsigned int messageStringLength = [messageString length]; |
|---|
| | 173 | unsigned int originalEmoticonLocation = NSNotFound; |
|---|
| | 174 | |
|---|
| | 175 | //Find the next occurence of a suspected emoticon |
|---|
| | 176 | *currentLocation = [messageString rangeOfCharacterFromSet:[self emoticonStartCharacterSet] |
|---|
| | 177 | options:NSLiteralSearch |
|---|
| | 178 | range:NSMakeRange(*currentLocation, |
|---|
| | 179 | messageStringLength - *currentLocation)].location; |
|---|
| | 180 | if (*currentLocation != NSNotFound) { |
|---|
| | 181 | //Use paired arrays so multiple emoticons can qualify for the same text equivalent |
|---|
| | 182 | NSMutableArray *candidateEmoticons = nil; |
|---|
| | 183 | NSMutableArray *candidateEmoticonTextEquivalents = nil; |
|---|
| | 184 | unichar currentCharacter = [messageString characterAtIndex:*currentLocation]; |
|---|
| | 185 | NSString *currentCharacterString = [NSString stringWithFormat:@"%C", currentCharacter]; |
|---|
| | 186 | NSEnumerator *emoticonEnumerator; |
|---|
| | 187 | AIEmoticon *emoticon; |
|---|
| | 188 | |
|---|
| | 189 | //Check for the presence of all emoticons starting with this character |
|---|
| | 190 | emoticonEnumerator = [[[self emoticonIndex] objectForKey:currentCharacterString] objectEnumerator]; |
|---|
| | 191 | while ((emoticon = [emoticonEnumerator nextObject])) { |
|---|
| | 192 | NSEnumerator *textEnumerator; |
|---|
| | 193 | NSString *text; |
|---|
| | 194 | |
|---|
| | 195 | textEnumerator = [[emoticon textEquivalents] objectEnumerator]; |
|---|
| | 196 | while ((text = [textEnumerator nextObject])) { |
|---|
| | 197 | int textLength = [text length]; |
|---|
| | 198 | |
|---|
| | 199 | if (textLength != 0) { //Invalid emoticon files may let empty text equivalents sneak in |
|---|
| | 200 | //If there is not enough room in the string for this text, we can skip it |
|---|
| | 201 | if (*currentLocation + textLength <= messageStringLength) { |
|---|
| | 202 | if ([messageString compare:text |
|---|
| | 203 | options:NSLiteralSearch |
|---|
| | 204 | range:NSMakeRange(*currentLocation, textLength)] == NSOrderedSame) { |
|---|
| | 205 | //Ignore emoticons within links |
|---|
| | 206 | if ([originalAttributedString attribute:NSLinkAttributeName |
|---|
| | 207 | atIndex:*currentLocation |
|---|
| | 208 | effectiveRange:nil] == nil) { |
|---|
| | 209 | if (!candidateEmoticons) { |
|---|
| | 210 | candidateEmoticons = [[NSMutableArray alloc] init]; |
|---|
| | 211 | candidateEmoticonTextEquivalents = [[NSMutableArray alloc] init]; |
|---|
| | 212 | } |
|---|
| | 213 | |
|---|
| | 214 | [candidateEmoticons addObject:emoticon]; |
|---|
| | 215 | [candidateEmoticonTextEquivalents addObject:text]; |
|---|
| | 216 | } |
|---|
| | 217 | } |
|---|
| | 218 | } |
|---|
| | 219 | } |
|---|
| | 220 | } |
|---|
| | 221 | } |
|---|
| | 222 | |
|---|
| | 223 | if ([candidateEmoticons count]) { |
|---|
| | 224 | NSString *replacementString; |
|---|
| | 225 | NSMutableAttributedString *replacement; |
|---|
| | 226 | int textLength; |
|---|
| | 227 | NSRange emoticonRangeInNewMessage; |
|---|
| | 228 | int amountToIncreaseCurrentLocation = 0; |
|---|
| | 229 | |
|---|
| | 230 | originalEmoticonLocation = *currentLocation; |
|---|
| | 231 | |
|---|
| | 232 | //Use the most appropriate, longest string of those which could be used for the emoticon text we found here |
|---|
| | 233 | emoticon = [self _bestReplacementFromEmoticons:candidateEmoticons |
|---|
| | 234 | withEquivalents:candidateEmoticonTextEquivalents |
|---|
| | 235 | context:serviceClassContext |
|---|
| | 236 | equivalent:&replacementString |
|---|
| | 237 | equivalentLength:&textLength]; |
|---|
| | 238 | emoticonRangeInNewMessage = NSMakeRange(*currentLocation - *replacementCount, textLength); |
|---|
| | 239 | |
|---|
| | 240 | /* We want to show this emoticon if there is: |
|---|
| | 241 | * It begins or ends the string |
|---|
| | 242 | * It is bordered by spaces or line breaks on both sides |
|---|
| | 243 | * It is bordered by a period on the left and a space or line break the right |
|---|
| | 244 | * It is bordered by emoticons on both sides or by an emoticon on the left and a period, space, or line break on the right |
|---|
| | 245 | */ |
|---|
| | 246 | BOOL acceptable = NO; |
|---|
| | 247 | if ((messageStringLength == ((originalEmoticonLocation + textLength))) || //Ends the string |
|---|
| | 248 | (originalEmoticonLocation == 0)) { //Begins the string |
|---|
| | 249 | acceptable = YES; |
|---|
| | 250 | } |
|---|
| | 251 | if (!acceptable) { |
|---|
| | 252 | /* Bordered by spaces or line breaks, or by a period on the left and a space or a line break on the right |
|---|
| | 253 | * If we're being called recursively, we have a potential emoticon to our left; we only need to check the right. |
|---|
| | 254 | * This is also true if we're not being called recursively but there's an NSAttachmentAttribute to our left. |
|---|
| | 255 | * That will happen if, for example, the string is ":):) ". The first emoticon is at the start of the line and |
|---|
| | 256 | * so is immediately acceptable. The second should be acceptable because it is to the right of an emoticon and |
|---|
| | 257 | * the left of a space. |
|---|
| | 258 | */ |
|---|
| | 259 | char previousCharacter = [messageString characterAtIndex:(originalEmoticonLocation - 1)] ; |
|---|
| | 260 | char nextCharacter = [messageString characterAtIndex:(originalEmoticonLocation + textLength)] ; |
|---|
| | 261 | |
|---|
| | 262 | if ((callingRecursively || (previousCharacter == ' ') || (previousCharacter == '\n') || (previousCharacter == '\r') || (previousCharacter == '.') || |
|---|
| | 263 | (*newMessage && [*newMessage attribute:NSAttachmentAttributeName |
|---|
| | 264 | atIndex:(emoticonRangeInNewMessage.location - 1) |
|---|
| | 265 | effectiveRange:NULL])) && |
|---|
| | 266 | ((nextCharacter == ' ') || (nextCharacter == '\n') || (nextCharacter == '\r') || (nextCharacter == '.'))) { |
|---|
| | 267 | acceptable = YES; |
|---|
| | 268 | |
|---|
| | 269 | //We can skip the next character |
|---|
| | 270 | //amountToIncreaseCurrentLocation = 1; |
|---|
| | 271 | } |
|---|
| | 272 | } |
|---|
| | 273 | if (!acceptable) { |
|---|
| | 274 | /* If we still haven't determined it to be acceptable, look ahead. |
|---|
| | 275 | * If we do a replacement adjacent to this emoticon, we can do this one, too. |
|---|
| | 276 | */ |
|---|
| | 277 | unsigned int newCurrentLocation = *currentLocation; |
|---|
| | 278 | unsigned int nextEmoticonLocation; |
|---|
| | 279 | |
|---|
| | 280 | /* Call ourself recursively, starting just after the end of the current emoticon candidate |
|---|
| | 281 | * If the return value is not NSNotFound, an emoticon was found and replaced ahead of us. Discontinuous searching for the win. |
|---|
| | 282 | */ |
|---|
| | 283 | newCurrentLocation += textLength; |
|---|
| | 284 | nextEmoticonLocation = [self replaceAnEmoticonStartingAtLocation:&newCurrentLocation |
|---|
| | 285 | fromString:messageString |
|---|
| | 286 | originalAttributedString:originalAttributedString |
|---|
| | 287 | intoString:newMessage |
|---|
| | 288 | replacementCount:replacementCount |
|---|
| | 289 | callingRecursively:YES |
|---|
| | 290 | context:serviceClassContext]; |
|---|
| | 291 | if (nextEmoticonLocation != NSNotFound) { |
|---|
| | 292 | if (nextEmoticonLocation == (*currentLocation + textLength)) { |
|---|
| | 293 | /* The next emoticon is immediately after the candidate we're looking at right now. That means |
|---|
| | 294 | * our current candidate is in fact an emoticon (since it borders another emoticon). |
|---|
| | 295 | */ |
|---|
| | 296 | acceptable = YES; |
|---|
| | 297 | } |
|---|
| | 298 | } |
|---|
| | 299 | |
|---|
| | 300 | /* Whether the current candidate is acceptable or not, we can now skip ahead to just after the next emoticon if |
|---|
| | 301 | * there is one. If there isn't, we can skip ahead to the end of the string. |
|---|
| | 302 | * |
|---|
| | 303 | * We do -1 because we will do a +1 at the end of the loop no matter what. |
|---|
| | 304 | */ |
|---|
| | 305 | if (newCurrentLocation != NSNotFound) { |
|---|
| | 306 | amountToIncreaseCurrentLocation = (newCurrentLocation - *currentLocation) - 1; |
|---|
| | 307 | } else { |
|---|
| | 308 | amountToIncreaseCurrentLocation = (messageStringLength - *currentLocation) - 1; |
|---|
| | 309 | } |
|---|
| | 310 | } |
|---|
| | 311 | |
|---|
| | 312 | if (acceptable) { |
|---|
| | 313 | replacement = [emoticon attributedStringWithTextEquivalent:replacementString]; |
|---|
| | 314 | |
|---|
| | 315 | //grab the original attributes, to ensure that the background is not lost in a message consisting only of an emoticon |
|---|
| | 316 | [replacement addAttributes:[originalAttributedString attributesAtIndex:originalEmoticonLocation |
|---|
| | 317 | effectiveRange:nil] |
|---|
| | 318 | range:NSMakeRange(0,1)]; |
|---|
| | 319 | |
|---|
| | 320 | //insert the emoticon |
|---|
| | 321 | if (!(*newMessage)) *newMessage = [originalAttributedString mutableCopy]; |
|---|
| | 322 | [*newMessage replaceCharactersInRange:emoticonRangeInNewMessage |
|---|
| | 323 | withAttributedString:replacement]; |
|---|
| | 324 | |
|---|
| | 325 | //Update where we are in the original and replacement messages |
|---|
| | 326 | *replacementCount += textLength-1; |
|---|
| | 327 | *currentLocation += textLength-1; |
|---|
| | 328 | } else { |
|---|
| | 329 | //Didn't find an acceptable emoticon, so we should return NSNotFound |
|---|
| | 330 | originalEmoticonLocation = NSNotFound; |
|---|
| | 331 | } |
|---|
| | 332 | |
|---|
| | 333 | //If appropriate, skip ahead by amountToIncreaseCurrentLocation |
|---|
| | 334 | *currentLocation += amountToIncreaseCurrentLocation; |
|---|
| | 335 | } |
|---|
| | 336 | |
|---|
| | 337 | //Always increment the loop |
|---|
| | 338 | *currentLocation += 1; |
|---|
| | 339 | |
|---|
| | 340 | [candidateEmoticons release]; |
|---|
| | 341 | [candidateEmoticonTextEquivalents release]; |
|---|
| | 342 | } |
|---|
| | 343 | |
|---|
| | 344 | return originalEmoticonLocation; |
|---|
| | 345 | } |
|---|
| | 346 | |
|---|
| 186 | | //Find the next occurence of a suspected emoticon |
|---|
| 187 | | currentLocation = [messageString rangeOfCharacterFromSet:emoticonStartCharacterSet |
|---|
| 188 | | options:0 |
|---|
| 189 | | range:NSMakeRange(currentLocation, |
|---|
| 190 | | messageStringLength - currentLocation)].location; |
|---|
| 191 | | |
|---|
| 192 | | //Use paired arrays so multiple emoticons can qualify for the same text equivalent |
|---|
| 193 | | NSMutableArray *candidateEmoticons = nil; |
|---|
| 194 | | NSMutableArray *candidateEmoticonTextEquivalents = nil; |
|---|
| 195 | | |
|---|
| 196 | | if (currentLocation != NSNotFound) { |
|---|
| 197 | | unichar currentCharacter = [messageString characterAtIndex:currentLocation]; |
|---|
| 198 | | NSString *currentCharacterString = [NSString stringWithFormat:@"%C", currentCharacter]; |
|---|
| 199 | | NSEnumerator *emoticonEnumerator; |
|---|
| 200 | | AIEmoticon *emoticon; |
|---|
| 201 | | |
|---|
| 202 | | //Check for the presence of all emoticons starting with this character |
|---|
| 203 | | emoticonEnumerator = [[emoticonIndex objectForKey:currentCharacterString] objectEnumerator]; |
|---|
| 204 | | while ((emoticon = [emoticonEnumerator nextObject])) { |
|---|
| 205 | | NSEnumerator *textEnumerator; |
|---|
| 206 | | NSString *text; |
|---|
| 207 | | |
|---|
| 208 | | textEnumerator = [[emoticon textEquivalents] objectEnumerator]; |
|---|
| 209 | | while ((text = [textEnumerator nextObject])) { |
|---|
| 210 | | int textLength = [text length]; |
|---|
| 211 | | |
|---|
| 212 | | if (textLength != 0) { //Invalid emoticon files may let empty text equivalents sneak in |
|---|
| 213 | | //If there is not enough room in the string for this text, we can skip it |
|---|
| 214 | | if (currentLocation + textLength <= messageStringLength) { |
|---|
| 215 | | if ([messageString compare:text options:0 range:NSMakeRange(currentLocation, textLength)] == NSOrderedSame) { |
|---|
| 216 | | //Ignore emoticons within links |
|---|
| 217 | | if ([inMessage attribute:NSLinkAttributeName atIndex:currentLocation effectiveRange:nil] == nil) { |
|---|
| 218 | | if (!candidateEmoticons) { |
|---|
| 219 | | candidateEmoticons = [[NSMutableArray alloc] init]; |
|---|
| 220 | | candidateEmoticonTextEquivalents = [[NSMutableArray alloc] init]; |
|---|
| 221 | | } |
|---|
| 222 | | |
|---|
| 223 | | [candidateEmoticons addObject:emoticon]; |
|---|
| 224 | | [candidateEmoticonTextEquivalents addObject:text]; |
|---|
| 225 | | } |
|---|
| 226 | | } |
|---|
| 227 | | } |
|---|
| 228 | | } |
|---|
| 229 | | } |
|---|
| 230 | | } |
|---|
| 231 | | |
|---|
| 232 | | if ([candidateEmoticons count]) { |
|---|
| 233 | | NSString *replacementString; |
|---|
| 234 | | AIEmoticon *emoticon; |
|---|
| 235 | | NSMutableAttributedString *replacement; |
|---|
| 236 | | int textLength; |
|---|
| 237 | | |
|---|
| 238 | | //Use the most appropriate, longest string of those which could be used for the emoticon text we found here |
|---|
| 239 | | emoticon = [self _bestReplacementFromEmoticons:candidateEmoticons |
|---|
| 240 | | withEquivalents:candidateEmoticonTextEquivalents |
|---|
| 241 | | context:serviceClassContext |
|---|
| 242 | | equivalent:&replacementString |
|---|
| 243 | | equivalentLength:&textLength]; |
|---|
| 244 | | replacement = [emoticon attributedStringWithTextEquivalent:replacementString]; |
|---|
| 245 | | |
|---|
| 246 | | //grab the original attributes, to ensure that the background is not lost in a message consisting only of an emoticon |
|---|
| 247 | | [replacement addAttributes:[inMessage attributesAtIndex:currentLocation |
|---|
| 248 | | effectiveRange:nil] |
|---|
| 249 | | range:NSMakeRange(0,1)]; |
|---|
| 250 | | |
|---|
| 251 | | //insert the emoticon |
|---|
| 252 | | if (!newMessage) newMessage = [inMessage mutableCopy]; |
|---|
| 253 | | [newMessage replaceCharactersInRange:NSMakeRange(currentLocation - replacementCount, textLength) |
|---|
| 254 | | withAttributedString:replacement]; |
|---|
| 255 | | //Update where we are in the original and replacement messages |
|---|
| 256 | | replacementCount += textLength-1; |
|---|
| 257 | | currentLocation += textLength-1; |
|---|
| 258 | | |
|---|
| 259 | | //Invalidate the enumerators to stop scanning prematurely |
|---|
| 260 | | //textEnumerator = nil; emoticonEnumerator = nil; |
|---|
| 261 | | } |
|---|
| 262 | | |
|---|
| 263 | | //Move to the next possible location of an emoticon |
|---|
| 264 | | currentLocation++; |
|---|
| 265 | | } |
|---|
| 266 | | |
|---|
| 267 | | [candidateEmoticons release]; |
|---|
| 268 | | [candidateEmoticonTextEquivalents release]; |
|---|
| 269 | | } |
|---|
| 270 | | |
|---|
| 271 | | return newMessage ? [newMessage autorelease] : inMessage; |
|---|
| | 374 | [self replaceAnEmoticonStartingAtLocation:¤tLocation |
|---|
| | 375 | fromString:messageString |
|---|
| | 376 | originalAttributedString:inMessage |
|---|
| | 377 | intoString:&newMessage |
|---|
| | 378 | replacementCount:&replacementCount |
|---|
| | 379 | callingRecursively:NO |
|---|
| | 380 | context:serviceClassContext]; |
|---|
| | 381 | } |
|---|
| | 382 | |
|---|
| | 383 | return (newMessage ? [newMessage autorelease] : inMessage); |
|---|