Plugins/WebKit Message View/AIWebkitMessageViewStyle.m
author Thijs Alkemade <thijsalkemade@gmail.com>
Sun Jan 29 02:53:09 2012 +0100 (3 months ago)
changeset 4585 6c1dd8c3ee4a
parent 4356 a0d4ead5c91e
child 4643 ecae0b8c089a
child 4651 b0a07c7d813c
permissions -rw-r--r--
Avoid a lot of false positives of the static analyzer.
     1 /* 
     2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
     3  * with this source distribution.
     4  * 
     5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
     6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
     7  * or (at your option) any later version.
     8  * 
     9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
    10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
    11  * Public License for more details.
    12  * 
    13  * You should have received a copy of the GNU General Public License along with this program; if not,
    14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
    15  */
    16 
    17 #import "AIWebkitMessageViewStyle.h"
    18 #import <AIUtilities/AIColorAdditions.h>
    19 #import <AIUtilities/AIStringAdditions.h>
    20 #import <AIUtilities/AIDateFormatterAdditions.h>
    21 #import <AIUtilities/AIMutableStringAdditions.h>
    22 #import <Adium/AIAccount.h>
    23 #import <Adium/AIChat.h>
    24 #import <Adium/AIContentTopic.h>
    25 #import <Adium/AIContentContext.h>
    26 #import <Adium/AIContentMessage.h>
    27 #import <Adium/AIContentNotification.h>
    28 #import <Adium/AIContentObject.h>
    29 #import <Adium/AIContentStatus.h>
    30 #import <Adium/AIHTMLDecoder.h>
    31 #import <Adium/AIListObject.h>
    32 #import <Adium/AIListContact.h>
    33 #import <Adium/AIService.h>
    34 #import <Adium/ESFileTransfer.h>
    35 #import <Adium/AIServiceIcons.h>
    36 #import <Adium/AIContentControllerProtocol.h>
    37 #import <Adium/AIStatusIcons.h>
    38 
    39 //
    40 #define LEGACY_VERSION_THRESHOLD		3	//Styles older than this version are considered legacy
    41 #define MAX_KNOWN_WEBKIT_VERSION        4   //Styles newer than this version are unknown entities
    42 
    43 //
    44 #define KEY_WEBKIT_VERSION				@"MessageViewVersion"
    45 #define KEY_WEBKIT_VERSION_MIN			@"MessageViewVersion_MinimumCompatible"
    46 
    47 //BOM scripts for appending content.
    48 #define APPEND_MESSAGE_WITH_SCROLL		@"checkIfScrollToBottomIsNeeded(); appendMessage(\"%@\"); scrollToBottomIfNeeded();"
    49 #define APPEND_NEXT_MESSAGE_WITH_SCROLL	@"checkIfScrollToBottomIsNeeded(); appendNextMessage(\"%@\"); scrollToBottomIfNeeded();"
    50 #define APPEND_MESSAGE					@"appendMessage(\"%@\");"
    51 #define APPEND_NEXT_MESSAGE				@"appendNextMessage(\"%@\");"
    52 #define APPEND_MESSAGE_NO_SCROLL		@"appendMessageNoScroll(\"%@\");"
    53 #define	APPEND_NEXT_MESSAGE_NO_SCROLL	@"appendNextMessageNoScroll(\"%@\");"
    54 #define REPLACE_LAST_MESSAGE			@"replaceLastMessage(\"%@\");"
    55 
    56 #define TOPIC_MAIN_DIV					@"<div id=\"topic\"></div>"
    57 // We set back, when the user finishes editing, the correct topic, which wipes out the existance of the span before. We don't need to undo the dbl click action.
    58 #define TOPIC_INDIVIDUAL_WRAPPER		@"<span id=\"topicEdit\" ondblclick=\"this.setAttribute('contentEditable', true); this.focus();\">%@</span>"
    59 
    60 @interface NSString (NewSnowLeopardMethods)
    61 - (NSComparisonResult)localizedStandardCompare:(NSString *)string;
    62 @end
    63 
    64 @interface NSMutableString (AIKeywordReplacementAdditions)
    65 - (void) replaceKeyword:(NSString *)word withString:(NSString *)newWord;
    66 - (void) safeReplaceCharactersInRange:(NSRange)range withString:(NSString *)newWord;
    67 @end
    68 
    69 @implementation NSMutableString (AIKeywordReplacementAdditions)
    70 - (void) replaceKeyword:(NSString *)keyWord withString:(NSString *)newWord
    71 {
    72 	if(!keyWord) return;
    73 	if(!newWord) newWord = @"";
    74 	[self replaceOccurrencesOfString:keyWord
    75 						  withString:newWord
    76 							 options:NSLiteralSearch
    77 							   range:NSMakeRange(0.0f, [self length])];
    78 }
    79 
    80 - (void) safeReplaceCharactersInRange:(NSRange)range withString:(NSString *)newWord
    81 {
    82 	if (range.location == NSNotFound || range.length == 0) return;
    83 	if (!newWord) [self deleteCharactersInRange:range];
    84 	else [self replaceCharactersInRange:range withString:newWord];
    85 }
    86 @end
    87 
    88 //The old code built the paths itself, which follows the filesystem's case sensitivity, so some noobs named stuff wrong. 
    89 //NSBundle is always case sensitive, so those styles broke (they were already broken on case sensitive hfsx)
    90 //These methods only check for the all-lowercase variant, so are not suitable for general purpose use.
    91 @interface NSBundle (StupidCompatibilityHack)
    92 - (NSString *)semiCaseInsensitivePathForResource:(NSString *)res ofType:(NSString *)type;
    93 - (NSString *)semiCaseInsensitivePathForResource:(NSString *)res ofType:(NSString *)type inDirectory:(NSString *)dirpath;
    94 @end
    95 
    96 @implementation NSBundle (StupidCompatibilityHack)
    97 - (NSString *)semiCaseInsensitivePathForResource:(NSString *)res ofType:(NSString *)type
    98 {
    99 	NSString *path = [self pathForResource:res ofType:type];
   100 	if(!path)
   101 		path = [self pathForResource:[res lowercaseString] ofType:type];
   102 	return path;
   103 }
   104 
   105 - (NSString *)semiCaseInsensitivePathForResource:(NSString *)res ofType:(NSString *)type inDirectory:(NSString *)dirpath
   106 {
   107 	NSString *path = [self pathForResource:res ofType:type inDirectory:dirpath];
   108 	if(!path)
   109 		path = [self pathForResource:[res lowercaseString] ofType:type inDirectory:dirpath];
   110 	return path;
   111 }
   112 
   113 @end
   114 
   115 @interface AIWebkitMessageViewStyle ()
   116 - (id)initWithBundle:(NSBundle *)inBundle;
   117 - (void)_loadTemplates;
   118 - (void)releaseResources;
   119 - (NSMutableString *)_escapeStringForPassingToScript:(NSMutableString *)inString;
   120 - (NSString *)noVariantName;
   121 - (NSString *)iconPathForFileTransfer:(ESFileTransfer *)inObject;
   122 - (NSString *)statusIconPathForListObject:(AIListObject *)inObject;
   123 @end
   124 
   125 @implementation AIWebkitMessageViewStyle
   126 
   127 @synthesize activeVariant;
   128 
   129 + (id)messageViewStyleFromBundle:(NSBundle *)inBundle
   130 {
   131 	return [[[self alloc] initWithBundle:inBundle] autorelease];
   132 }
   133 
   134 + (id)messageViewStyleFromPath:(NSString *)path
   135 {
   136 	NSBundle *styleBundle = [NSBundle bundleWithPath:[path stringByExpandingBundlePath]];
   137 	if(styleBundle)
   138 		return [[[self alloc] initWithBundle:styleBundle] autorelease];
   139 	return nil;
   140 }
   141 
   142 /*!
   143  *	@brief Initialize
   144  */
   145 - (id)initWithBundle:(NSBundle *)inBundle
   146 {
   147 	if ((self = [super init])) {
   148 		styleBundle = [inBundle retain];
   149 		stylePath = [[styleBundle resourcePath] retain];
   150         
   151 		if ([self reloadStyle] == FALSE) {
   152             [self release];
   153             return nil;
   154         }
   155 	}
   156 
   157 	return self;
   158 }
   159 
   160 - (BOOL) reloadStyle
   161 {
   162 	[self releaseResources];
   163 	
   164 	/* Our styles are versioned so we can change how they work without breaking compatibility.
   165 	 *
   166 	 * Version 0: Initial Webkit Version
   167 	 * Version 1: Template.html now handles all scroll-to-bottom functionality.  It is no longer required to call the
   168 	 *            scrollToBottom functions when inserting content.
   169 	 * Version 2: No significant changes
   170 	 * Version 3: main.css is no longer a separate style, it now serves as the base stylesheet and is imported by default.
   171 	 *            The default variant is now a separate file in /variants like all other variants.
   172 	 *			  Template.html now includes appendMessageNoScroll() and appendNextMessageNoScroll() which behave
   173 	 *				the same as appendMessage() and appendNextMessage() in Versions 1 and 2 but without scrolling.
   174 	 * Version 4: Template.html now includes replaceLastMessage()
   175 	 *            Template.html now defines actionMessageUserName and actionMessageBody for display of /me (actions).
   176 	 *				 If the style provides a custom Template.html, these classes must be defined.
   177 	 *				 CSS can be used to customize the appearance of actions.
   178 	 *			  HTML filters in are now supported in Adium's content filter system; filters can assume Version 4 or later.
   179 	 */
   180 	styleVersion = [[styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_VERSION] integerValue];
   181 	
   182     /* Refuse to load a version whose minimum compatible version is greater than the latest version we know about; that
   183      * indicates this is a style FROM THE FUTURE, and we can't risk corrupting our own timeline.
   184      */
   185     NSInteger minimumCompatibleVersion = [[styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_VERSION_MIN] integerValue];
   186     if (minimumCompatibleVersion && (minimumCompatibleVersion > MAX_KNOWN_WEBKIT_VERSION)) {
   187         return NO;
   188     }
   189 
   190     //Default behavior
   191 	allowTextBackgrounds = YES;
   192 
   193 	//Pre-fetch our templates
   194 	[self _loadTemplates];
   195 	
   196 	//Style flags
   197 	allowsCustomBackground = ![[styleBundle objectForInfoDictionaryKey:@"DisableCustomBackground"] boolValue];
   198 	transparentDefaultBackground = [[styleBundle objectForInfoDictionaryKey:@"DefaultBackgroundIsTransparent"] boolValue];
   199 	
   200 	combineConsecutive = ![[styleBundle objectForInfoDictionaryKey:@"DisableCombineConsecutive"] boolValue];
   201 	
   202 	NSNumber *tmpNum = [styleBundle objectForInfoDictionaryKey:@"ShowsUserIcons"];
   203 	allowsUserIcons = (tmpNum ? [tmpNum boolValue] : YES);
   204 	
   205 	//User icon masking
   206 	NSString *tmpName = [styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_USER_ICON_MASK];
   207 	if (tmpName) userIconMask = [[NSImage alloc] initWithContentsOfFile:[stylePath stringByAppendingPathComponent:tmpName]];
   208 	
   209 	NSNumber *allowsColorsNumber = [styleBundle objectForInfoDictionaryKey:@"AllowTextColors"];
   210 	allowsColors = (allowsColorsNumber ? [allowsColorsNumber boolValue] : YES);
   211     
   212     return YES;
   213 }
   214 
   215 /*!
   216  *  @brief release everything we loaded from the style bundle
   217  */
   218 - (void)releaseResources
   219 {
   220 	//Templates
   221 	[headerHTML release];
   222 	[footerHTML release];
   223 	[baseHTML release];
   224 	[contentHTML release];
   225 	[contentInHTML release];
   226 	[nextContentInHTML release];
   227 	[contextInHTML release];
   228 	[nextContextInHTML release];
   229 	[contentOutHTML release];
   230 	[nextContentOutHTML release];
   231 	[contextOutHTML release];
   232 	[nextContextOutHTML release];
   233 	[statusHTML release];	
   234 	[fileTransferHTML release];
   235 	[topicHTML release];
   236 		
   237 	[customBackgroundPath release];
   238 	[customBackgroundColor release];
   239 	
   240 	[userIconMask release];
   241 }
   242 
   243 /*!
   244  *	@brief Deallocate
   245  */
   246 - (void)dealloc
   247 {	
   248 	[styleBundle release];
   249 	[stylePath release];
   250 
   251 	[self releaseResources];
   252 	[timeStampFormatter release];
   253 	
   254 	[[NSDistributedNotificationCenter defaultCenter] removeObserver: self];
   255 	
   256 	[statusIconPathCache release];
   257 	[timeFormatterCache release];
   258 
   259 	self.activeVariant = nil;
   260 	
   261 	[super dealloc];
   262 }
   263 
   264 @synthesize bundle = styleBundle;
   265 
   266 - (BOOL)isLegacy
   267 {
   268 	return styleVersion < LEGACY_VERSION_THRESHOLD;
   269 }
   270 
   271 #pragma mark Settings
   272 
   273 @synthesize allowsCustomBackground, allowsUserIcons, allowsColors, userIconMask;
   274 
   275 - (NSArray *)validSenderColors
   276 {
   277 	if(!checkedSenderColors) {
   278 		NSURL *url = [NSURL fileURLWithPath:[stylePath stringByAppendingPathComponent:@"Incoming/SenderColors.txt"]];
   279 		NSString *senderColorsFile = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:NULL];
   280 		
   281 		if(senderColorsFile)
   282 			validSenderColors = [[senderColorsFile componentsSeparatedByString:@":"] retain];
   283 		
   284 		checkedSenderColors = YES;
   285 	}
   286 	
   287 	return validSenderColors;
   288 }
   289 
   290 - (BOOL)isBackgroundTransparent
   291 {
   292 	//Our custom background is only transparent if the user has set a custom color with an alpha component less than 1.0
   293 	return ((!customBackgroundColor && transparentDefaultBackground) ||
   294 		   (customBackgroundColor && [customBackgroundColor alphaComponent] < 0.99));
   295 }
   296 
   297 - (NSString *)defaultFontFamily
   298 {
   299 	NSString *defaultFontFamily = [styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_DEFAULT_FONT_FAMILY];
   300 	if (!defaultFontFamily) defaultFontFamily = [[NSFont systemFontOfSize:0] familyName];
   301 	
   302 	return defaultFontFamily;
   303 }
   304 
   305 - (NSNumber *)defaultFontSize
   306 {
   307 	NSNumber *defaultFontSize = [styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_DEFAULT_FONT_SIZE];
   308 
   309 	if (!defaultFontSize) defaultFontSize = [NSNumber numberWithInteger:[[NSFont systemFontOfSize:0] pointSize]];
   310 	
   311 	return defaultFontSize;
   312 }
   313 
   314 - (BOOL)hasHeader
   315 {
   316 	return headerHTML && [headerHTML length];
   317 }
   318 
   319 - (BOOL)hasTopic
   320 {
   321 	return topicHTML && [topicHTML length];
   322 }
   323 
   324 #pragma mark Behavior
   325 
   326 - (void)setDateFormat:(NSString *)format
   327 {
   328 	if (!format || [format length] == 0) {
   329 		format = [NSDateFormatter localizedDateFormatStringShowingSeconds:NO showingAMorPM:NO];
   330 	}
   331 
   332 	[timeStampFormatter release];
   333 
   334 	if ([format rangeOfString:@"%"].location != NSNotFound) {
   335 		/* Support strftime-style format strings, which old message styles may use */
   336 		timeStampFormatter = [[NSDateFormatter alloc] initWithDateFormat:format allowNaturalLanguage:NO];
   337 	} else {
   338 		timeStampFormatter = [[NSDateFormatter alloc] init];
   339 		[timeStampFormatter	setFormatterBehavior:NSDateFormatterBehavior10_4];
   340 		[timeStampFormatter setDateFormat:format];
   341 	}
   342 }
   343 
   344 - (void) flushTimeFormatterCache:(id)dummy {
   345 	[timeFormatterCache removeAllObjects];
   346 }
   347 
   348 @synthesize allowTextBackgrounds, customBackgroundType, customBackgroundColor, showIncomingMessageColors=showIncomingColors, showIncomingMessageFonts=showIncomingFonts, customBackgroundPath, nameFormat, useCustomNameFormat, showHeader, showUserIcons;
   349 
   350 //Templates ------------------------------------------------------------------------------------------------------------
   351 #pragma mark Templates
   352 - (NSString *)baseTemplateForChat:(AIChat *)chat
   353 {
   354 	NSMutableString	*templateHTML;
   355 
   356 	// If this is a group chat, we want to include a topic.
   357 	// Otherwise, if the header is shown, use it.
   358 	NSString *headerContent = @"";
   359 	if (showHeader) {
   360 		if (chat.isGroupChat) {
   361 			headerContent = (chat.supportsTopic ? TOPIC_MAIN_DIV : @"");
   362 		} else if (headerHTML) {
   363 			headerContent = headerHTML;
   364 		}
   365 	}
   366 	
   367 	//Old styles may be using an old custom 4 parameter baseHTML.  Styles version 3 and higher should
   368 	//be using the bundled (or a custom) 5 parameter baseHTML.
   369 	if ((styleVersion < 3) && usingCustomTemplateHTML) {
   370 		templateHTML = [NSMutableString stringWithFormat:baseHTML,						//Template
   371 			[[NSURL fileURLWithPath:stylePath] absoluteString],							//Base path
   372 			[self pathForVariant:self.activeVariant],									//Variant path
   373 			headerContent,
   374 			(footerHTML ? footerHTML : @"")];
   375 	} else {		
   376 		templateHTML = [NSMutableString stringWithFormat:baseHTML,						//Template
   377 			[[NSURL fileURLWithPath:stylePath] absoluteString],							//Base path
   378 			styleVersion < 3 ? @"" : @"@import url( \"main.css\" );",					//Import main.css for new enough styles
   379 			[self pathForVariant:self.activeVariant],									//Variant path
   380 			headerContent,
   381 			(footerHTML ? footerHTML : @"")];
   382 	}
   383 
   384 	return [self fillKeywordsForBaseTemplate:templateHTML chat:chat];
   385 }
   386 
   387 - (NSString *)templateForContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar
   388 {
   389 	NSString	*template;
   390 	
   391 	//Get the correct template for what we're inserting
   392 	if ([[content type] isEqualToString:CONTENT_MESSAGE_TYPE]) {
   393 		if ([content isOutgoing]) {
   394 			template = (contentIsSimilar ? nextContentOutHTML : contentOutHTML);
   395 		} else {
   396 			template = (contentIsSimilar ? nextContentInHTML : contentInHTML);
   397 		}
   398 	
   399 	} else if ([[content type] isEqualToString:CONTENT_CONTEXT_TYPE]) {
   400 		if ([content isOutgoing]) {
   401 			template = (contentIsSimilar ? nextContextOutHTML : contextOutHTML);
   402 		} else {
   403 			template = (contentIsSimilar ? nextContextInHTML : contextInHTML);
   404 		}
   405 
   406 	} else if([[content type] isEqualToString:CONTENT_FILE_TRANSFER_TYPE]) {
   407 		template = [[fileTransferHTML mutableCopy] autorelease];
   408 	} else if ([[content type] isEqualToString:CONTENT_TOPIC_TYPE]) {
   409 		template = topicHTML;
   410 	}
   411 	else {
   412 		template = statusHTML;
   413 	}
   414 	
   415 	return template;
   416 }
   417 
   418 - (NSString *)completedTemplateForContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar
   419 {
   420 	NSMutableString *mutableTemplate = [[self templateForContent:content similar:contentIsSimilar] mutableCopy];
   421 	
   422 	if (mutableTemplate)
   423 		[self fillKeywords:mutableTemplate forContent:content similar:contentIsSimilar];
   424 	
   425 	return [mutableTemplate autorelease];
   426 }
   427 
   428 /*!
   429  *	@brief Pre-fetch all the style templates
   430  *
   431  *	This needs to be called before either baseTemplate or templateForContent is called
   432  */
   433 - (void)_loadTemplates
   434 {		
   435 	//Load the style's templates
   436 	//We can't use NSString's initWithContentsOfFile here.  HTML files are interpreted in the defaultCEncoding
   437 	//(which varies by system) when read that way.  We want to always interpret the files as UTF8.
   438 	headerHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Header" ofType:@"html"]] retain];
   439 	footerHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Footer" ofType:@"html"]] retain];
   440 	topicHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Topic" ofType:@"html"]] retain];
   441 	baseHTML = [NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Template" ofType:@"html"]];
   442 	
   443 	//Starting with version 1, styles can choose to not include template.html.  If the template is not included 
   444 	//Adium's default will be used.  This is preferred since any future template updates will apply to the style
   445 	if ((!baseHTML || [baseHTML length] == 0) && styleVersion >= 1) {		
   446 		baseHTML = [NSString stringWithContentsOfUTF8File:[[NSBundle bundleForClass:[self class]] semiCaseInsensitivePathForResource:@"Template" ofType:@"html"]];
   447 		usingCustomTemplateHTML = NO;
   448 	} else {
   449 		usingCustomTemplateHTML = YES;
   450 		
   451 		NSAssert(baseHTML != nil, @"The impossible happened!");
   452 		
   453 		if ([baseHTML rangeOfString:@"function imageCheck()" options:NSLiteralSearch].location != NSNotFound) {
   454 			/* This doesn't quite fix image swapping on styles with broken image swapping due to custom HTML templates,
   455 			 * but it improves it. For some reason, the result of using our normal template.html functions is that 
   456 			 * clicking works once, then the text doesn't allow a return click. This is an improvement compared
   457 			 * to fully broken behavior in which the return click shows a missing-image placeholder.
   458 			 */
   459 			NSMutableString *imageSwapFixedBaseHTML = [[baseHTML mutableCopy] autorelease];
   460 			[imageSwapFixedBaseHTML replaceOccurrencesOfString:
   461 			 @"		function imageCheck() {\n"
   462 			 "			node = event.target;\n"
   463 			 "			if(node.tagName == 'IMG' && node.alt) {\n"
   464 			 "				a = document.createElement('a');\n"
   465 			 "				a.setAttribute('onclick', 'imageSwap(this)');\n"
   466 			 "				a.setAttribute('src', node.src);\n"
   467 			 "				text = document.createTextNode(node.alt);\n"
   468 			 "				a.appendChild(text);\n"
   469 			 "				node.parentNode.replaceChild(a, node);\n"
   470 			 "			}\n"
   471 			 "		}"
   472 													withString:
   473 			 @"		function imageCheck() {\n"
   474 			 "			var node = event.target;\n"
   475 			 "			if(node.tagName.toLowerCase() == 'img' && !client.zoomImage(node) && node.alt) {\n"
   476 			 "				var a = document.createElement('a');\n"
   477 			 "				a.setAttribute('onclick', 'imageSwap(this)');\n"
   478 			 "				a.setAttribute('src', node.getAttribute('src'));\n"
   479 			 "				a.className = node.className;\n"
   480 			 "				var text = document.createTextNode(node.alt);\n"
   481 			 "				a.appendChild(text);\n"
   482 			 "				node.parentNode.replaceChild(a, node);\n"
   483 			 "			}\n"
   484 			 "		}"
   485 													   options:NSLiteralSearch];
   486 			[imageSwapFixedBaseHTML replaceOccurrencesOfString:
   487 			 @"		function imageSwap(node) {\n"
   488 			 "			img = document.createElement('img');\n"
   489 			 "			img.setAttribute('src', node.src);\n"
   490 			 "			img.setAttribute('alt', node.firstChild.nodeValue);\n"
   491 			 "			node.parentNode.replaceChild(img, node);\n"
   492 			 "			alignChat();\n"
   493 			 "		}"
   494 													withString:
   495 			 @"		function imageSwap(node) {\n"
   496 			 "			var shouldScroll = nearBottom();\n"
   497 			 "			//Swap the image/text\n"
   498 			 "			var img = document.createElement('img');\n"
   499 			 "			img.setAttribute('src', node.getAttribute('src'));\n"
   500 			 "			img.setAttribute('alt', node.firstChild.nodeValue);\n"
   501 			 "			img.className = node.className;\n"
   502 			 "			node.parentNode.replaceChild(img, node);\n"
   503 			 "			\n"
   504 			 "			alignChat(shouldScroll);\n"
   505 			 "		}"
   506 													   options:NSLiteralSearch];
   507 			/* Now for ones which don't call alignChat() */
   508 			[imageSwapFixedBaseHTML replaceOccurrencesOfString:
   509 			 @"		function imageSwap(node) {\n"
   510 			 "			img = document.createElement('img');\n"
   511 			 "			img.setAttribute('src', node.src);\n"
   512 			 "			img.setAttribute('alt', node.firstChild.nodeValue);\n"
   513 			 "			node.parentNode.replaceChild(img, node);\n"
   514 			 "		}"
   515 													withString:
   516 			 @"		function imageSwap(node) {\n"
   517 			 "			var shouldScroll = nearBottom();\n"
   518 			 "			//Swap the image/text\n"
   519 			 "			var img = document.createElement('img');\n"
   520 			 "			img.setAttribute('src', node.getAttribute('src'));\n"
   521 			 "			img.setAttribute('alt', node.firstChild.nodeValue);\n"
   522 			 "			img.className = node.className;\n"
   523 			 "			node.parentNode.replaceChild(img, node);\n"
   524 			 "		}"
   525 													   options:NSLiteralSearch];
   526 			baseHTML = imageSwapFixedBaseHTML;
   527 		}
   528 		
   529 	}
   530 	[baseHTML retain];
   531 	
   532 	//Content Templates
   533 	contentHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Content" ofType:@"html"]] retain];
   534 	contentInHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Content" ofType:@"html" inDirectory:@"Incoming"]] retain];
   535 	nextContentInHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"NextContent" ofType:@"html" inDirectory:@"Incoming"]] retain];
   536 	contentOutHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Content" ofType:@"html" inDirectory:@"Outgoing"]] retain];
   537 	nextContentOutHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"NextContent" ofType:@"html" inDirectory:@"Outgoing"]] retain];
   538 	
   539 	//Message history
   540 	contextInHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Context" ofType:@"html" inDirectory:@"Incoming"]] retain];
   541 	nextContextInHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"NextContext" ofType:@"html" inDirectory:@"Incoming"]] retain];
   542 	contextOutHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Context" ofType:@"html" inDirectory:@"Outgoing"]] retain];
   543 	nextContextOutHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"NextContext" ofType:@"html" inDirectory:@"Outgoing"]] retain];
   544 	
   545 	//Fall back to Resources/Content.html if Incoming isn't present
   546 	if (!contentInHTML) contentInHTML = [contentHTML retain];
   547 	
   548 	//Fall back to Content if NextContent doesn't need to use different HTML
   549 	if (!nextContentInHTML) nextContentInHTML = [contentInHTML retain];
   550 	
   551 	//Fall back to Content if Context isn't present
   552 	if (!nextContextInHTML) nextContextInHTML = [nextContentInHTML retain];
   553 	if (!contextInHTML) contextInHTML = [contentInHTML retain];
   554 	
   555 	//Fall back to Content if Context isn't present
   556 	if (!nextContextOutHTML && nextContentOutHTML) nextContextOutHTML = [nextContentOutHTML retain];
   557 	if (!contextOutHTML && contentOutHTML) contextOutHTML = [contentOutHTML retain];
   558 	
   559 	//Fall back to Content if Context isn't present
   560 	if (!nextContextOutHTML) nextContextOutHTML = [nextContextInHTML retain];
   561 	if (!contextOutHTML) contextOutHTML = [contextInHTML retain];
   562 	
   563 	//Fall back to Incoming if Outgoing doesn't need to be different
   564 	if (!contentOutHTML) contentOutHTML = [contentInHTML retain];
   565 	if (!nextContentOutHTML) nextContentOutHTML = [nextContentInHTML retain];
   566 	
   567 	//Status
   568 	statusHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Status" ofType:@"html"]] retain];
   569 	
   570 	//Fall back to Resources/Incoming/Content.html if Status isn't present
   571 	if (!statusHTML) statusHTML = [contentInHTML retain];
   572 	
   573 	//TODO: make a generic Request message, rather than having this ft specific one
   574 	NSMutableString *fileTransferHTMLTemplate;
   575 	fileTransferHTMLTemplate = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"FileTransferRequest" ofType:@"html"]] mutableCopy];
   576 	if(!fileTransferHTMLTemplate) {
   577 		fileTransferHTMLTemplate = [contentInHTML mutableCopy];
   578 		[fileTransferHTMLTemplate replaceKeyword:@"%message%"
   579 									  withString:@"<p><img src=\"%fileIconPath%\" style=\"width:32px; height:32px; vertical-align:middle;\"></img><input type=\"button\" onclick=\"%saveFileAsHandler%\" value=\"Download %fileName%\"></p>"];
   580 	}
   581 	[fileTransferHTMLTemplate replaceKeyword:@"Download %fileName%"
   582 						  withString:[NSString stringWithFormat:AILocalizedString(@"Download %@", "%@ will be a file name"), @"%fileName%"]];
   583 	fileTransferHTML = fileTransferHTMLTemplate;
   584 }
   585 
   586 #pragma mark Scripts
   587 - (NSString *)scriptForAppendingContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar willAddMoreContentObjects:(BOOL)willAddMoreContentObjects replaceLastContent:(BOOL)replaceLastContent
   588 {
   589 	NSMutableString	*newHTML;
   590 	NSString		*script;
   591 	
   592 	//If combining of consecutive messages has been disabled, we treat all content as non-similar
   593 	if (!combineConsecutive) contentIsSimilar = NO;
   594 	
   595 	//Fetch the correct template and substitute keywords for the passed content
   596 	newHTML = [[[self completedTemplateForContent:content similar:contentIsSimilar] mutableCopy] autorelease];
   597 	
   598 	//BOM scripts vary by style version
   599 	if (!usingCustomTemplateHTML && styleVersion >= 4) {
   600 		/* If we're using the built-in template HTML, we know that it supports our most modern scripts */
   601 		if (replaceLastContent)
   602 			script = REPLACE_LAST_MESSAGE;
   603 		else if (willAddMoreContentObjects) {
   604 			script = (contentIsSimilar ? APPEND_NEXT_MESSAGE_NO_SCROLL : APPEND_MESSAGE_NO_SCROLL);
   605 		} else {
   606 			script = (contentIsSimilar ? APPEND_NEXT_MESSAGE : APPEND_MESSAGE);
   607 		}
   608 		
   609 	} else  if (styleVersion >= 3) {
   610 		if (willAddMoreContentObjects) {
   611 			script = (contentIsSimilar ? APPEND_NEXT_MESSAGE_NO_SCROLL : APPEND_MESSAGE_NO_SCROLL);
   612 		} else {
   613 			script = (contentIsSimilar ? APPEND_NEXT_MESSAGE : APPEND_MESSAGE);
   614 		}
   615 	} else if (styleVersion >= 1) {
   616 		script = (contentIsSimilar ? APPEND_NEXT_MESSAGE : APPEND_MESSAGE);
   617 		
   618 	} else {
   619 		if (usingCustomTemplateHTML && [content isKindOfClass:[AIContentStatus class]]) {
   620 			/* Old styles with a custom template.html had Status.html files without 'insert' divs coupled 
   621 			 * with a APPEND_NEXT_MESSAGE_WITH_SCROLL script which assumes one exists.
   622 			 */
   623 			script = APPEND_MESSAGE_WITH_SCROLL;
   624 		} else {
   625 			script = (contentIsSimilar ? APPEND_NEXT_MESSAGE_WITH_SCROLL : APPEND_MESSAGE_WITH_SCROLL);
   626 		}
   627 	}
   628 	
   629 	return [NSString stringWithFormat:script, [self _escapeStringForPassingToScript:newHTML]]; 
   630 }
   631 
   632 - (NSString *)scriptForChangingVariant
   633 {
   634 	return [NSString stringWithFormat:@"setStylesheet(\"mainStyle\",\"%@\");",[self pathForVariant:self.activeVariant]];
   635 }
   636 
   637 - (NSString *)scriptForScrollingAfterAddingMultipleContentObjects
   638 {
   639 	if ((styleVersion >= 3) || !usingCustomTemplateHTML) {
   640 		return @"alignChat(true);";
   641 	}
   642 
   643 	return nil;
   644 }
   645 
   646 /*!
   647  *	@brief Escape a string for passing to our BOM scripts
   648  */
   649 - (NSMutableString *)_escapeStringForPassingToScript:(NSMutableString *)inString
   650 {	
   651 	//We need to escape a few things to get our string to the javascript without trouble
   652 	[inString replaceOccurrencesOfString:@"\\" 
   653 							  withString:@"\\\\" 
   654 								 options:NSLiteralSearch];
   655 	
   656 	[inString replaceOccurrencesOfString:@"\"" 
   657 							  withString:@"\\\"" 
   658 								 options:NSLiteralSearch];
   659 		
   660 	[inString replaceOccurrencesOfString:@"\n" 
   661 							  withString:@"" 
   662 								 options:NSLiteralSearch];
   663 
   664 	[inString replaceOccurrencesOfString:@"\r" 
   665 							  withString:@"<br>" 
   666 								 options:NSLiteralSearch];
   667 
   668 	return inString;
   669 }
   670 
   671 #pragma mark Variants
   672 
   673 - (NSArray *)availableVariants
   674 {
   675 	NSMutableArray	*availableVariants = [NSMutableArray array];
   676 	
   677 	//Build an array of all variant names
   678 	for (NSString *path in [styleBundle pathsForResourcesOfType:@"css" inDirectory:@"Variants"]) {
   679 		[availableVariants addObject:[[path lastPathComponent] stringByDeletingPathExtension]];
   680 	}
   681 
   682 	//Style versions before 3 stored the default variant in a separate location.  They also allowed for this
   683 	//varient name to not be specified, and would substitute a localized string in its place.
   684 	if (styleVersion < 3) {
   685 		[availableVariants addObject:[self noVariantName]];
   686 	}
   687 	
   688 	//Alphabetize the variants
   689 	[availableVariants sortUsingSelector:@selector(localizedStandardCompare:)];
   690 	
   691 	return availableVariants;
   692 }
   693 
   694 - (NSString *)pathForVariant:(NSString *)variant
   695 {
   696 	//Styles before version 3 stored the default variant in main.css, and not in the variants folder.
   697 	if (styleVersion < 3 && [variant isEqualToString:[self noVariantName]]) {
   698 		return @"main.css";
   699 	} else {
   700 		return [NSString stringWithFormat:@"Variants/%@.css",variant];
   701 	}
   702 }
   703 
   704 /*!
   705  *	@brief Base variant name for styles before version 2
   706  */
   707 - (NSString *)noVariantName
   708 {
   709 	NSString	*noVariantName = [styleBundle objectForInfoDictionaryKey:@"DisplayNameForNoVariant"];
   710 	return noVariantName ? noVariantName : AILocalizedString(@"Normal","Normal style variant menu item");
   711 }
   712 
   713 + (NSString *)noVariantNameForBundle:(NSBundle *)inBundle
   714 {
   715 	NSString	*noVariantName = [inBundle objectForInfoDictionaryKey:@"DisplayNameForNoVariant"];
   716 	return noVariantName ? noVariantName : AILocalizedString(@"Normal","Normal style variant menu item");	
   717 }
   718 
   719 - (NSString *)defaultVariant
   720 {
   721 	return styleVersion < 3 ? [self noVariantName] : [styleBundle objectForInfoDictionaryKey:@"DefaultVariant"];
   722 }
   723 
   724 + (NSString *)defaultVariantForBundle:(NSBundle *)inBundle
   725 {
   726 	return [[inBundle objectForInfoDictionaryKey:KEY_WEBKIT_VERSION] integerValue] < 3 ? 
   727 		   [self noVariantNameForBundle:inBundle] : 
   728 		   [inBundle objectForInfoDictionaryKey:@"DefaultVariant"];
   729 }
   730 
   731 #pragma mark Keyword replacement
   732 
   733 - (NSMutableString *)fillKeywords:(NSMutableString *)inString forContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar
   734 {
   735 	NSDate			*date = nil;
   736 	NSRange			range;
   737 	AIListObject	*contentSource = [content source];
   738 	AIListObject	*theSource = ([contentSource isKindOfClass:[AIListContact class]] ?
   739 								  [(AIListContact *)contentSource parentContact] :
   740 								  contentSource);
   741 
   742 	/*
   743 		htmlEncodedMessage is only encoded correctly for AIContentMessages
   744 		but we do it up here so that we can check for RTL/LTR text below without
   745 		having to encode the message twice. This is less than ideal 
   746 	 */
   747 	NSString		*htmlEncodedMessage = [AIHTMLDecoder encodeHTML:[content message]
   748 															headers:NO 
   749 														   fontTags:showIncomingFonts
   750 												 includingColorTags:(allowsColors && showIncomingColors)
   751 													  closeFontTags:YES
   752 														  styleTags:YES
   753 										 closeStyleTagsOnFontChange:YES
   754 													 encodeNonASCII:YES
   755 													   encodeSpaces:YES
   756 														 imagesPath:NSTemporaryDirectory()
   757 												  attachmentsAsText:NO
   758 										  onlyIncludeOutgoingImages:NO
   759 													 simpleTagsOnly:NO
   760 													 bodyBackground:NO
   761 										        allowJavascriptURLs:NO];
   762 	
   763 	if (styleVersion >= 4)
   764 		htmlEncodedMessage = [adium.contentController filterHTMLString:htmlEncodedMessage
   765 															   direction:[content isOutgoing] ? AIFilterOutgoing : AIFilterIncoming
   766 																 content:content];
   767 
   768 	//date
   769 	if ([content respondsToSelector:@selector(date)])
   770 		date = [(AIContentMessage *)content date];
   771 	
   772 	//Replacements applicable to any AIContentObject
   773 	[inString replaceKeyword:@"%time%" 
   774 				  withString:(date ? [timeStampFormatter stringFromDate:date] : @"")];
   775 
   776 	__block NSString *shortTimeString;
   777 	[NSDateFormatter withLocalizedDateFormatterShowingSeconds:NO showingAMorPM:NO perform:^(NSDateFormatter *dateFormatter){
   778 		shortTimeString = (date ? [[dateFormatter stringFromDate:date] retain] : @"");
   779 	}];
   780 	[shortTimeString autorelease];
   781 	
   782 	[inString replaceKeyword:@"%shortTime%"
   783 				  withString:shortTimeString];
   784 
   785 	if ([inString rangeOfString:@"%senderStatusIcon%"].location != NSNotFound) {
   786 		//Only cache the status icon to disk if the message style will actually use it
   787 		[inString replaceKeyword:@"%senderStatusIcon%"
   788 					  withString:[self statusIconPathForListObject:theSource]];
   789 	}
   790 	
   791 	//Replaces %localized{x}% with a a localized version of x, searching the style's localizations, and then Adium's localizations
   792 	do{
   793 		range = [inString rangeOfString:@"%localized{"];
   794 		if (range.location != NSNotFound) {
   795 			NSRange endRange;
   796 			endRange = [inString rangeOfString:@"}%" options:NSLiteralSearch range:NSMakeRange(NSMaxRange(range), [inString length] - NSMaxRange(range))];
   797 			if (endRange.location != NSNotFound && endRange.location > NSMaxRange(range)) {
   798 				NSString *untranslated = [inString substringWithRange:NSMakeRange(NSMaxRange(range), (endRange.location - NSMaxRange(range)))];
   799 				
   800 				NSString *translated = [styleBundle localizedStringForKey:untranslated
   801 																	value:untranslated
   802 																	table:nil];
   803 				if (!translated || [translated length] == 0) {
   804 					translated = [[NSBundle bundleForClass:[self class]] localizedStringForKey:untranslated
   805 																						 value:untranslated
   806 																						 table:nil];
   807 					if (!translated || [translated length] == 0) {
   808 						translated = [[NSBundle mainBundle] localizedStringForKey:untranslated
   809 																			value:untranslated
   810 																			table:nil];
   811 					}
   812 				}
   813 				
   814 				
   815 				[inString safeReplaceCharactersInRange:NSUnionRange(range, endRange) 
   816 											withString:translated];
   817 			}
   818 		}
   819 	} while (range.location != NSNotFound);
   820 
   821 	[inString replaceKeyword:@"%userIcons%"
   822 				  withString:(showUserIcons ? @"showIcons" : @"hideIcons")];
   823 
   824 	[inString replaceKeyword:@"%messageClasses%"
   825 				  withString:[(contentIsSimilar ? @"consecutive " : @"") stringByAppendingString:[[content displayClasses] componentsJoinedByString:@" "]]];
   826 	
   827 	[inString replaceKeyword:@"%senderColor%"
   828 				  withString:[NSColor representedColorForObject:contentSource.UID withValidColors:self.validSenderColors]];
   829 	
   830 	//HAX. The odd conditional here detects the rtl html that our html parser spits out.
   831 	BOOL isRTL = ([htmlEncodedMessage rangeOfString:@"<div dir=\"rtl\">"
   832                                             options:(NSCaseInsensitiveSearch | NSLiteralSearch)].location != NSNotFound);
   833 	[inString replaceKeyword:@"%messageDirection%"
   834 				  withString:(isRTL ? @"rtl" : @"ltr")];
   835 	
   836 	//Replaces %time{x}% with a timestamp formatted like x (using NSDateFormatter)
   837 	do{
   838 		range = [inString rangeOfString:@"%time{"];
   839 		if (range.location != NSNotFound) {
   840 			NSRange endRange;
   841 			endRange = [inString rangeOfString:@"}%" options:NSLiteralSearch range:NSMakeRange(NSMaxRange(range), [inString length] - NSMaxRange(range))];
   842 			if (endRange.location != NSNotFound && endRange.location > NSMaxRange(range)) {
   843 				if (date) {
   844 					if (!timeFormatterCache) {
   845 						timeFormatterCache = [[NSMutableDictionary alloc] init];
   846 						[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(flushTimeFormatterCache:) name:@"AppleDatePreferencesChangedNotification" object:nil];
   847 						[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(flushTimeFormatterCache:) name:@"AppleTimePreferencesChangedNotification" object:nil];
   848 					}
   849 					NSString *timeFormat = [inString substringWithRange:NSMakeRange(NSMaxRange(range), (endRange.location - NSMaxRange(range)))];
   850 					
   851 					NSDateFormatter *dateFormatter = [timeFormatterCache objectForKey:timeFormat];
   852 					if (!dateFormatter) {
   853 						if ([timeFormat rangeOfString:@"%"].location != NSNotFound) {
   854 							/* Support strftime-style format strings, which old message styles may use */
   855 							dateFormatter = [[NSDateFormatter alloc] initWithDateFormat:timeFormat allowNaturalLanguage:NO];
   856 						} else {
   857 							dateFormatter = [[NSDateFormatter alloc] init];
   858 							[dateFormatter	setFormatterBehavior:NSDateFormatterBehavior10_4];
   859 							[dateFormatter setDateFormat:timeFormat];
   860 						}
   861 						[timeFormatterCache setObject:dateFormatter forKey:timeFormat];
   862 						[dateFormatter release];
   863 					}
   864 					
   865 					[inString safeReplaceCharactersInRange:NSUnionRange(range, endRange) 
   866 												withString:[dateFormatter stringFromDate:date]];
   867 					
   868 				} else
   869 					[inString deleteCharactersInRange:NSUnionRange(range, endRange)];
   870 				
   871 			}
   872 		}
   873 	} while (range.location != NSNotFound);
   874 	
   875 	do{
   876 		range = [inString rangeOfString:@"%userIconPath%"];
   877 		if (range.location != NSNotFound) {
   878 			NSString    *userIconPath;
   879 			NSString	*replacementString;
   880 			
   881 			userIconPath = [theSource valueForProperty:KEY_WEBKIT_USER_ICON];
   882 			if (!userIconPath) {
   883 				userIconPath = [theSource valueForProperty:@"UserIconPath"];
   884 			}
   885 			
   886 			if (showUserIcons && userIconPath) {
   887 				replacementString = [NSString stringWithFormat:@"file://%@", userIconPath];
   888 				
   889 			} else {
   890 				replacementString = ([content isOutgoing]
   891 									 ? @"Outgoing/buddy_icon.png" 
   892 									 : @"Incoming/buddy_icon.png");
   893 			}
   894 			
   895 			[inString safeReplaceCharactersInRange:range withString:replacementString];
   896 		}
   897 	} while (range.location != NSNotFound);
   898 	
   899 	[inString replaceKeyword:@"%service%" 
   900 				  withString:[content.chat.account.service shortDescription]];
   901 	
   902 	[inString replaceKeyword:@"%serviceIconPath%"
   903 				  withString:[AIServiceIcons pathForServiceIconForServiceID:content.chat.account.service.serviceID
   904 																	   type:AIServiceIconLarge]];
   905 
   906 	if ([inString rangeOfString:@"%variant%"].location != NSNotFound) {
   907 		/* Per #12702, don't allow spaces in the variant name, as otherwise it becomes multiple css classes */
   908 		[inString replaceKeyword:@"%variant%"
   909 					  withString:[self.activeVariant stringByReplacingOccurrencesOfString:@" " withString:@"_"]];
   910 	}
   911 
   912 	//message stuff
   913 	if ([content isKindOfClass:[AIContentMessage class]]) {
   914 		
   915 		//Use [content source] directly rather than the potentially-metaContact theSource
   916 		NSString *formattedUID = nil;
   917 		if ([content.chat aliasForContact:contentSource]) {
   918 			formattedUID = [content.chat aliasForContact:contentSource];
   919 		} else {
   920 			formattedUID = contentSource.formattedUID;
   921 		}
   922 
   923 		NSString *displayName = [content.chat displayNameForContact:contentSource];
   924 		
   925 		[inString replaceKeyword:@"%status%"
   926 					  withString:@""];
   927 
   928 		[inString replaceKeyword:@"%senderScreenName%" 
   929 					  withString:[(formattedUID ?
   930 								   formattedUID :
   931 								   displayName) stringByEscapingForXMLWithEntities:nil]];
   932 		 
   933         
   934 		[inString replaceKeyword:@"%senderPrefix%"
   935 					  withString:((AIContentMessage *)content).senderPrefix];
   936 		
   937 		do{
   938 			range = [inString rangeOfString:@"%sender%"];
   939 			if (range.location != NSNotFound) {
   940 				NSString		*senderDisplay = nil;
   941 				if (useCustomNameFormat) {
   942 			 		if (formattedUID && ![displayName isEqualToString:formattedUID]) {
   943 						switch (nameFormat) {
   944 							case AIDefaultName:
   945 								break;
   946 
   947 							case AIDisplayName:
   948 								senderDisplay = displayName;
   949 								break;
   950 
   951 							case AIDisplayName_ScreenName:
   952 								senderDisplay = [NSString stringWithFormat:@"%@ (%@)",displayName,formattedUID];
   953 								break;
   954 
   955 							case AIScreenName_DisplayName:
   956 								senderDisplay = [NSString stringWithFormat:@"%@ (%@)",formattedUID,displayName];
   957 								break;
   958 
   959 							case AIScreenName:
   960 								senderDisplay = formattedUID;
   961 								break;
   962 						}
   963 					}
   964 
   965 					//Test both displayName and formattedUID for nil-ness. If they're both nil, the assertion will trip.
   966 					if (!senderDisplay) {
   967 						senderDisplay = displayName;
   968 						if (!senderDisplay) {
   969 							senderDisplay = formattedUID;
   970 							if (!senderDisplay) {
   971 								AILog(@"XXX we don't have a sender for %@ (%@)", content, [content message]);
   972 								NSLog(@"Enormous error: we don't have a sender for %@ (%@)", content, [content message]);
   973 								
   974 								// This shouldn't happen.
   975 								senderDisplay = @"(unknown)";
   976 							}
   977 						}
   978 					}
   979 				} else {
   980 					senderDisplay = displayName;
   981 				}
   982 				
   983 				if ([(AIContentMessage *)content isAutoreply]) {
   984 					senderDisplay = [NSString stringWithFormat:@"%@ %@",senderDisplay,AILocalizedString(@"(Autoreply)","Short word inserted after the sender's name when displaying a message which was an autoresponse")];
   985 				}
   986 					
   987 				[inString safeReplaceCharactersInRange:range withString:[senderDisplay stringByEscapingForXMLWithEntities:nil]];
   988 			}
   989 		} while (range.location != NSNotFound);
   990         
   991 		do {
   992 			range = [inString rangeOfString:@"%senderDisplayName%"];
   993 			if (range.location != NSNotFound) {
   994 				NSString *serversideDisplayName = ([theSource isKindOfClass:[AIListContact class]] ?
   995 												   [(AIListContact *)theSource serversideDisplayName] :
   996 												   nil);
   997 				if (!serversideDisplayName) {
   998 					serversideDisplayName = theSource.displayName;
   999 				}
  1000 				
  1001 				[inString safeReplaceCharactersInRange:range
  1002 											withString:[serversideDisplayName stringByEscapingForXMLWithEntities:nil]];
  1003 			}
  1004 		} while (range.location != NSNotFound);
  1005 
  1006 		//Blatantly stealing the date code for the background color script.
  1007 		do{
  1008 			range = [inString rangeOfString:@"%textbackgroundcolor{"];
  1009 			if (range.location != NSNotFound) {
  1010 				NSRange endRange;
  1011 				endRange = [inString rangeOfString:@"}%" options:NSLiteralSearch range:NSMakeRange(NSMaxRange(range), [inString length] - NSMaxRange(range))];
  1012 				if (endRange.location != NSNotFound && endRange.location > NSMaxRange(range)) {
  1013 					NSString *transparency = [inString substringWithRange:NSMakeRange(NSMaxRange(range),
  1014 																					  (endRange.location - NSMaxRange(range)))];
  1015 					
  1016 					if (allowTextBackgrounds && showIncomingColors) {
  1017 						NSString *thisIsATemporaryString;
  1018 						unsigned rgb = 0, red, green, blue;
  1019 						NSScanner *hexcode;
  1020 						thisIsATemporaryString = [AIHTMLDecoder encodeHTML:[content message] headers:NO 
  1021 																  fontTags:NO
  1022 														includingColorTags:NO
  1023 															 closeFontTags:NO
  1024 																 styleTags:NO
  1025 												closeStyleTagsOnFontChange:NO
  1026 															encodeNonASCII:NO
  1027 															  encodeSpaces:NO
  1028 																imagesPath:NSTemporaryDirectory()
  1029 														 attachmentsAsText:NO
  1030 												 onlyIncludeOutgoingImages:NO
  1031 															simpleTagsOnly:NO
  1032 															bodyBackground:YES
  1033 													   allowJavascriptURLs:NO];
  1034 						hexcode = [NSScanner scannerWithString:thisIsATemporaryString];
  1035 						[hexcode scanHexInt:&rgb];
  1036 						if (![thisIsATemporaryString length] && rgb == 0) {
  1037 							[inString deleteCharactersInRange:NSUnionRange(range, endRange)];
  1038 						} else {
  1039 							red = (rgb & 0xff0000) >> 16;
  1040 							green = (rgb & 0x00ff00) >> 8;
  1041 							blue = rgb & 0x0000ff;
  1042 							[inString safeReplaceCharactersInRange:NSUnionRange(range, endRange)
  1043 														withString:[NSString stringWithFormat:@"rgba(%d, %d, %d, %@)", red, green, blue, transparency]];
  1044 						}
  1045 					} else {
  1046 						[inString deleteCharactersInRange:NSUnionRange(range, endRange)];
  1047 					}
  1048 				} else if (endRange.location == NSMaxRange(range)) {
  1049 					if (allowTextBackgrounds && showIncomingColors) {
  1050 						NSString *thisIsATemporaryString;
  1051 						
  1052 						thisIsATemporaryString = [AIHTMLDecoder encodeHTML:[content message] headers:NO 
  1053 																  fontTags:NO
  1054 														includingColorTags:NO
  1055 															 closeFontTags:NO
  1056 																 styleTags:NO
  1057 												closeStyleTagsOnFontChange:NO
  1058 															encodeNonASCII:NO
  1059 															  encodeSpaces:NO
  1060 																imagesPath:NSTemporaryDirectory()
  1061 														 attachmentsAsText:NO
  1062 												 onlyIncludeOutgoingImages:NO
  1063 															simpleTagsOnly:NO
  1064 															bodyBackground:YES
  1065 													   allowJavascriptURLs:NO];
  1066 						[inString safeReplaceCharactersInRange:NSUnionRange(range, endRange) 
  1067 													withString:[NSString stringWithFormat:@"#%@", thisIsATemporaryString]];
  1068 					} else {
  1069 						[inString deleteCharactersInRange:NSUnionRange(range, endRange)];
  1070 					}	
  1071 				}
  1072 			}
  1073 		} while (range.location != NSNotFound);
  1074 
  1075 		if ([content isKindOfClass:[ESFileTransfer class]]) { //file transfers are an AIContentMessage subclass
  1076 		
  1077 			ESFileTransfer *transfer = (ESFileTransfer *)content;
  1078 			NSString *fileName = [[transfer remoteFilename] stringByEscapingForXMLWithEntities:nil];
  1079 			NSString *fileTransferID = [[transfer uniqueID] stringByEscapingForXMLWithEntities:nil];
  1080 
  1081 			range = [inString rangeOfString:@"%fileIconPath%"];
  1082 			if (range.location != NSNotFound) {
  1083 				NSString *iconPath = [self iconPathForFileTransfer:transfer];
  1084 				NSImage *icon = [transfer iconImage];
  1085 				do{
  1086 					[[icon TIFFRepresentation] writeToFile:iconPath atomically:YES];
  1087 					[inString safeReplaceCharactersInRange:range withString:iconPath];
  1088 					range = [inString rangeOfString:@"%fileIconPath%"];
  1089 				} while (range.location != NSNotFound);
  1090 			}
  1091 
  1092 			[inString replaceKeyword:@"%fileName%"
  1093 						  withString:fileName];
  1094 			
  1095 			[inString replaceKeyword:@"%saveFileHandler%"
  1096 						  withString:[NSString stringWithFormat:@"client.handleFileTransfer('Save', '%@')", fileTransferID]];
  1097 			
  1098 			[inString replaceKeyword:@"%saveFileAsHandler%"
  1099 						  withString:[NSString stringWithFormat:@"client.handleFileTransfer('SaveAs', '%@')", fileTransferID]];
  1100 			
  1101 			[inString replaceKeyword:@"%cancelRequestHandler%"
  1102 						  withString:[NSString stringWithFormat:@"client.handleFileTransfer('Cancel', '%@')", fileTransferID]];
  1103 		}
  1104 
  1105 		//Message (must do last)
  1106 		range = [inString rangeOfString:@"%message%"];
  1107 		while(range.location != NSNotFound) {
  1108 			[inString safeReplaceCharactersInRange:range withString:htmlEncodedMessage];
  1109 			range = [inString rangeOfString:@"%message%"
  1110 									options:NSLiteralSearch
  1111 									  range:NSMakeRange(range.location + htmlEncodedMessage.length,
  1112 														inString.length - range.location - htmlEncodedMessage.length)];
  1113 		} 
  1114 		
  1115 		// Topic replacement (if applicable)
  1116 		if ([content isKindOfClass:[AIContentTopic class]]) {
  1117 			range = [inString rangeOfString:@"%topic%"];
  1118 			
  1119 			if (range.location != NSNotFound) {
  1120 				[inString safeReplaceCharactersInRange:range withString:[NSString stringWithFormat:TOPIC_INDIVIDUAL_WRAPPER, htmlEncodedMessage]];
  1121 			}
  1122 		}		
  1123 	} else if ([content isKindOfClass:[AIContentStatus class]]) {
  1124 		NSString	*statusPhrase;
  1125 		BOOL		replacedStatusPhrase = NO;
  1126 		
  1127 		[inString replaceKeyword:@"%status%" 
  1128 				  withString:[[(AIContentStatus *)content status] stringByEscapingForXMLWithEntities:nil]];
  1129 		
  1130 		[inString replaceKeyword:@"%statusSender%" 
  1131 				  withString:[theSource.displayName stringByEscapingForXMLWithEntities:nil]];
  1132 
  1133 		[inString replaceKeyword:@"%senderScreenName%"
  1134 				  withString:@""];
  1135 
  1136 		[inString replaceKeyword:@"%senderPrefix%"
  1137 				  withString:@""];
  1138 
  1139 		[inString replaceKeyword:@"%sender%"
  1140 				  withString:@""];
  1141 
  1142 		if ((statusPhrase = [[content userInfo] objectForKey:@"Status Phrase"])) {
  1143 			do{
  1144 				range = [inString rangeOfString:@"%statusPhrase%"];
  1145 				if (range.location != NSNotFound) {
  1146 					[inString safeReplaceCharactersInRange:range 
  1147 												withString:[statusPhrase stringByEscapingForXMLWithEntities:nil]];
  1148 					replacedStatusPhrase = YES;
  1149 				}
  1150 			} while (range.location != NSNotFound);
  1151 		}
  1152 		
  1153 		//Message (must do last)
  1154 		range = [inString rangeOfString:@"%message%"];
  1155 		if (range.location != NSNotFound) {
  1156 			NSString	*messageString;
  1157 
  1158 			if (replacedStatusPhrase) {
  1159 				//If the status phrase was used, clear the message tag
  1160 				messageString = @"";
  1161 			} else {
  1162 				messageString = [AIHTMLDecoder encodeHTML:[content message]
  1163 												  headers:NO 
  1164 												 fontTags:NO
  1165 									   includingColorTags:NO
  1166 											closeFontTags:YES
  1167 												styleTags:NO
  1168 							   closeStyleTagsOnFontChange:YES
  1169 										   encodeNonASCII:YES
  1170 											 encodeSpaces:YES
  1171 											   imagesPath:NSTemporaryDirectory()
  1172 										attachmentsAsText:NO
  1173 								onlyIncludeOutgoingImages:NO
  1174 										   simpleTagsOnly:NO
  1175 										   bodyBackground:NO
  1176 									  allowJavascriptURLs:NO];
  1177 			}
  1178 			
  1179 			[inString safeReplaceCharactersInRange:range withString:messageString];
  1180 		}
  1181 	}
  1182 
  1183 	return inString;
  1184 }
  1185 
  1186 - (NSMutableString *)fillKeywordsForBaseTemplate:(NSMutableString *)inString chat:(AIChat *)chat
  1187 {
  1188 	NSRange	range;
  1189 	
  1190 	[inString replaceKeyword:@"%chatName%"
  1191 				  withString:[chat.displayName stringByEscapingForXMLWithEntities:nil]];
  1192 
  1193 	NSString * sourceName = [chat.account.displayName stringByEscapingForXMLWithEntities:nil];
  1194 	if(!sourceName) sourceName = @" ";
  1195 	[inString replaceKeyword:@"%sourceName%"
  1196 				  withString:sourceName];
  1197 	
  1198 	NSString *destinationName = chat.listObject.displayName;
  1199 	if (!destinationName) destinationName = chat.displayName;
  1200 	[inString replaceKeyword:@"%destinationName%"
  1201 				  withString:destinationName];
  1202 	
  1203 	NSString *serversideDisplayName = chat.listObject.serversideDisplayName;
  1204 	if (!serversideDisplayName) serversideDisplayName = chat.displayName;
  1205 	[inString replaceKeyword:@"%destinationDisplayName%"
  1206 				  withString:[serversideDisplayName stringByEscapingForXMLWithEntities:nil]];
  1207 		
  1208 	AIListContact	*listObject = chat.listObject;
  1209 	NSString		*iconPath = nil;
  1210 	
  1211 	[inString replaceKeyword:@"%incomingColor%"
  1212 				  withString:[NSColor representedColorForObject:listObject.UID withValidColors:self.validSenderColors]];
  1213 	
  1214 	[inString replaceKeyword:@"%outgoingColor%"
  1215 				  withString:[NSColor representedColorForObject:chat.account.UID withValidColors:self.validSenderColors]];
  1216 	
  1217 	if (listObject) {
  1218 		iconPath = [listObject valueForProperty:KEY_WEBKIT_USER_ICON];
  1219 		if (!iconPath)
  1220 			iconPath = [listObject valueForProperty:@"UserIconPath"];
  1221 		
  1222 		/* We couldn't get an icon... but perhaps we can for a parent contact */
  1223 		if (!iconPath &&
  1224 			[listObject isKindOfClass:[AIListContact class]] &&
  1225 			([(AIListContact *)listObject parentContact] != listObject)) {
  1226 			iconPath = [[(AIListContact *)listObject parentContact] valueForProperty:KEY_WEBKIT_USER_ICON];
  1227 			if (!iconPath)
  1228 				iconPath = [[(AIListContact *)listObject parentContact] valueForProperty:@"UserIconPath"];			
  1229 		}		
  1230 	}
  1231 	[inString replaceKeyword:@"%incomingIconPath%"
  1232 				  withString:(iconPath ? iconPath : @"incoming_icon.png")];
  1233 
  1234 	AIListObject	*account = chat.account;
  1235 	iconPath = nil;
  1236 	
  1237 	if (account) {
  1238 		iconPath = [account valueForProperty:KEY_WEBKIT_USER_ICON];
  1239 		if (!iconPath)
  1240 			iconPath = [account valueForProperty:@"UserIconPath"];
  1241 	}
  1242 	[inString replaceKeyword:@"%outgoingIconPath%"
  1243 				  withString:(iconPath ? iconPath : @"outgoing_icon.png")];
  1244 
  1245 	NSString *serviceIconPath = [AIServiceIcons pathForServiceIconForServiceID:account.service.serviceID
  1246 																		  type:AIServiceIconLarge];
  1247 	
  1248 	NSString *serviceIconTag = [NSString stringWithFormat:@"<img class=\"serviceIcon\" src=\"%@\" alt=\"%@\" title=\"%@\">", serviceIconPath ? serviceIconPath : @"outgoing_icon.png", [account.service shortDescription], [account.service shortDescription]];
  1249 	
  1250 	[inString replaceKeyword:@"%service%" 
  1251 				  withString:[account.service shortDescription]];
  1252 	
  1253 	[inString replaceKeyword:@"%serviceIconImg%"
  1254 				  withString:serviceIconTag];
  1255 	
  1256 	[inString replaceKeyword:@"%serviceIconPath%"
  1257 				  withString:serviceIconPath];
  1258 	
  1259 	[inString replaceKeyword:@"%timeOpened%"
  1260 				  withString:[timeStampFormatter stringFromDate:[chat dateOpened]]];
  1261 	
  1262 	//Replaces %time{x}% with a timestamp formatted like x (using NSDateFormatter)
  1263 	do{
  1264 		range = [inString rangeOfString:@"%timeOpened{"];
  1265 		if (range.location != NSNotFound) {
  1266 			NSRange endRange;
  1267 			endRange = [inString rangeOfString:@"}%" options:NSLiteralSearch range:NSMakeRange(NSMaxRange(range), [inString length] - NSMaxRange(range))];
  1268 
  1269 			if (endRange.location != NSNotFound && endRange.location > NSMaxRange(range)) {				
  1270 				NSString		*timeFormat = [inString substringWithRange:NSMakeRange(NSMaxRange(range), (endRange.location - NSMaxRange(range)))];
  1271 				
  1272 				NSDateFormatter *dateFormatter;
  1273 				if ([timeFormat rangeOfString:@"%"].location != NSNotFound) {
  1274 					/* Support strftime-style format strings, which old message styles may use */
  1275 					dateFormatter = [[NSDateFormatter alloc] initWithDateFormat:timeFormat allowNaturalLanguage:NO];
  1276 				} else {
  1277 					dateFormatter = [[NSDateFormatter alloc] init];
  1278 					[dateFormatter	setFormatterBehavior:NSDateFormatterBehavior10_4];
  1279 					[dateFormatter setDateFormat:timeFormat];
  1280 				}
  1281 				
  1282 				[inString safeReplaceCharactersInRange:NSUnionRange(range, endRange) 
  1283 												withString:[dateFormatter stringFromDate:[chat dateOpened]]];
  1284 				[dateFormatter release];
  1285 				
  1286 			}
  1287 		}
  1288 	} while (range.location != NSNotFound);
  1289 	
  1290 	[NSDateFormatter withLocalizedDateFormatterPerform:^(NSDateFormatter *dateFormatter){
  1291 		[inString replaceKeyword:@"%dateOpened%"
  1292 					  withString:[dateFormatter stringFromDate:[chat dateOpened]]];
  1293 	}];
  1294 	
  1295 	//Background
  1296 	{
  1297 		range = [inString rangeOfString:@"==bodyBackground=="];
  1298 		
  1299 		if (range.location != NSNotFound) { //a backgroundImage tag is not required
  1300 			NSMutableString *bodyTag = nil;
  1301 
  1302 			if (allowsCustomBackground && (customBackgroundPath || customBackgroundColor)) {				
  1303 				bodyTag = [[[NSMutableString alloc] init] autorelease];
  1304 				
  1305 				if (customBackgroundPath) {
  1306 					if ([customBackgroundPath length]) {
  1307 						switch (customBackgroundType) {
  1308 							case BackgroundNormal:
  1309 								[bodyTag appendString:[NSString stringWithFormat:@"background-image: url('%@'); background-repeat: no-repeat; background-attachment:fixed;", customBackgroundPath]];
  1310 							break;
  1311 							case BackgroundCenter:
  1312 								[bodyTag appendString:[NSString stringWithFormat:@"background-image: url('%@'); background-position: center; background-repeat: no-repeat; background-attachment:fixed;", customBackgroundPath]];
  1313 							break;
  1314 							case BackgroundTile:
  1315 								[bodyTag appendString:[NSString stringWithFormat:@"background-image: url('%@'); background-repeat: repeat;", customBackgroundPath]];
  1316 							break;
  1317 							case BackgroundTileCenter:
  1318 								[bodyTag appendString:[NSString stringWithFormat:@"background-image: url('%@'); background-repeat: repeat; background-position: center;", customBackgroundPath]];
  1319 							break;
  1320 							case BackgroundScale:
  1321 								[bodyTag appendString:[NSString stringWithFormat:@"background-image: url('%@'); -webkit-background-size: 100%% 100%%; background-size: 100%% 100%%; background-attachment: fixed;", customBackgroundPath]];
  1322 							break;
  1323 						}
  1324 					} else {
  1325 						[bodyTag appendString:@"background-image: none; "];
  1326 					}
  1327 				}
  1328 				if (customBackgroundColor) {
  1329 					CGFloat red, green, blue, alpha;
  1330 					[customBackgroundColor getRed:&red green:&green blue:&blue alpha:&alpha];
  1331 					[bodyTag appendString:[NSString stringWithFormat:@"background-color: rgba(%ld, %ld, %ld, %f); ", (NSInteger)(red * 255.0), (NSInteger)(green * 255.0), (NSInteger)(blue * 255.0), alpha]];
  1332 				}
  1333  			}
  1334 			
  1335 			//Replace the body background tag
  1336  			[inString safeReplaceCharactersInRange:range withString:(bodyTag ? (NSString *)bodyTag : @"")];
  1337  		}
  1338  	}
  1339 	
  1340 	if ([inString rangeOfString:@"%variant%"].location != NSNotFound) {
  1341 		/* Per #12702, don't allow spaces in the variant name, as otherwise it becomes multiple css classes */
  1342 		[inString replaceKeyword:@"%variant%"
  1343 					  withString:[self.activeVariant stringByReplacingOccurrencesOfString:@" " withString:@"_"]];
  1344 	}
  1345 	
  1346 	return inString;
  1347 }
  1348 
  1349 #pragma mark Icons
  1350 
  1351 - (NSString *)iconPathForFileTransfer:(ESFileTransfer *)inObject
  1352 {
  1353 	NSString	*filename = [NSString stringWithFormat:@"TEMP-%@%@.tiff", [inObject uniqueID], [NSString randomStringOfLength:5]];
  1354 	return [[adium cachesPath] stringByAppendingPathComponent:filename];
  1355 }
  1356 
  1357 - (NSString *)statusIconPathForListObject:(AIListObject *)inObject
  1358 {
  1359 	if(!statusIconPathCache) statusIconPathCache = [[NSMutableDictionary alloc] init];
  1360 	NSImage *icon = [AIStatusIcons statusIconForListObject:inObject
  1361 													  type:AIStatusIconTab
  1362 												 direction:AIIconNormal];
  1363 	NSString *statusName = [AIStatusIcons statusNameForListObject:inObject];
  1364 	if(!statusName)
  1365 		statusName = @"UnknownStatus";
  1366 	NSString *path = [statusIconPathCache objectForKey:statusName];
  1367 	if(!path)
  1368 	{
  1369 		path = [[adium cachesPath] stringByAppendingPathComponent:[NSString stringWithFormat:@"TEMP-%@%@.tiff", statusName, [NSString randomStringOfLength:5]]];
  1370 		[[icon TIFFRepresentation] writeToFile:path atomically:YES];
  1371 		[statusIconPathCache setObject:path forKey:statusName];
  1372 	}
  1373 
  1374 	return path;
  1375 }
  1376 
  1377 @end