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