Plugins/WebKit Message View/AIWebkitMessageViewStyle.m
author Evan Schoenberg
Sun Nov 22 20:06:12 2009 -0600 (2009-11-22)
changeset 2939 9f11b96baf1d
parent 2892 ab4ed45fa4b8
child 2954 31f6f4c62d78
permissions -rw-r--r--
If we try to update icons while the webview doesn't have an accessible documentFrame (e.g. while it is still working on loading its initial HTML), try again after a second. This fixes icons which are part of the initial bolus of information presented by a webview that change shortly after it loads, which can most noticeably happen when restoring a chat using a webview with icons in its header but could also occur if a user icon changed as we opened a chat window.

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