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