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