Plugins/WebKit Message View/AIWebkitMessageViewStyle.m
author Zachary West <zacw@adium.im>
Wed Oct 28 01:57:39 2009 -0400 (2009-10-28)
changeset 2789 d70bc57935ad
parent 2771 7e41d8e52be8
child 2799 3ee009cf02f0
permissions -rw-r--r--
Address a few font issues that have cropped up recently. Fixes #12906.

In previous releases of Adium, we would drop (in a few situations) the font tag information for outgoing messages if it was Helvetica sized 12. This, of course, was terribly confusing: if you specified any attributes, such as color, weight, decoration, etc., it forced Helvetica again.

In Snow Leopard, Apple started including the background color by default in our attributed strings, which has been causing us to, once again, force font tags in situations where we wouldn't before. Sometimes you'd see Helvetica, sometimes you wouldn't. Confusing.

Instead of trying to send plaintext, which we failed at in a good amount of situations (and all situations in SL), we now always send the font information. This was done by removing the forced Helvetica/12 assumptions in the HTML decoder.

We also respect the "show message fonts" preference for outgoing messages on top of incoming. This was another part of the confusing affects that a non-forced default of Helvetica was causing. Outgoing would appear Helvetica, incoming would be the non-allowed default WKMV font.

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