Frameworks/Adium Framework/Source/AIHTMLDecoder.m
author Zachary West <zacw@adium.im>
Wed Oct 28 10:49:09 2009 -0400 (2009-10-28)
changeset 2667 3b3d1d6c5394
parent 2660 6a8a206aa8af
child 2912 f1a416077965
permissions -rw-r--r--
Restore back AIHTMLDecoder's assumption that fonts are Helvetica and the sky is blue.

This fixes the "italics" case talked about in adium-1.4/a81e3542ed8f. Refs #12906.

Pre-SL behavior now fully achieved. Hooray.
David@0
     1
/*-------------------------------------------------------------------------------------------------------*\
David@0
     2
| Adium, Copyright (C) 2001-2006, Adam Iser  (adamiser@mac.com | http://www.adiumx.com)                   |
David@0
     3
\---------------------------------------------------------------------------------------------------------/
David@0
     4
 | This program is free software; you can redistribute it and/or modify it under the terms of the GNU
David@0
     5
 | General Public License as published by the Free Software Foundation; either version 2 of the License,
David@0
     6
 | or (at your option) any later version.
David@0
     7
 |
David@0
     8
 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
David@0
     9
 | the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
David@0
    10
 | Public License for more details.
David@0
    11
 |
David@0
    12
 | You should have received a copy of the GNU General Public License along with this program; if not,
David@0
    13
 | write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
David@0
    14
 \------------------------------------------------------------------------------------------------------ */
David@0
    15
David@0
    16
/*
David@0
    17
	A quick and simple HTML to Attributed string converter (ha! --jmelloy)
David@0
    18
*/
David@0
    19
David@0
    20
#import <Adium/AIHTMLDecoder.h>
David@0
    21
David@0
    22
#import <AIUtilities/AIApplicationAdditions.h>
David@0
    23
#import <AIUtilities/AITextAttributes.h>
David@0
    24
#import <AIUtilities/AIAttributedStringAdditions.h>
David@0
    25
#import <AIUtilities/AIColorAdditions.h>
David@0
    26
#import <AIUtilities/AIDictionaryAdditions.h>
David@0
    27
#import <AIUtilities/AIStringAdditions.h>
David@0
    28
#import <AIUtilities/AIFileManagerAdditions.h>
David@0
    29
#import <AIUtilities/AIImageAdditions.h>
David@0
    30
#import <AIUtilities/AIFileManagerAdditions.h>
David@0
    31
David@0
    32
#import <Adium/AITextAttachmentExtension.h>
David@0
    33
#import <Adium/ESFileWrapperExtension.h>
zacw@1226
    34
#import <Adium/AIXMLElement.h>
David@0
    35
David@0
    36
#import <FriBidi/NSString-FBAdditions.h>
David@0
    37
catfish@2093
    38
#import <CoreServices/CoreServices.h>
David@0
    39
David@0
    40
int HTMLEquivalentForFontSize(int fontSize);
David@0
    41
David@84
    42
@interface AIHTMLDecoder ()
David@0
    43
- (NSDictionary *)processFontTagArgs:(NSDictionary *)inArgs attributes:(AITextAttributes *)textAttributes;
David@0
    44
- (void)processBodyTagArgs:(NSDictionary *)inArgs attributes:(AITextAttributes *)textAttributes;
David@0
    45
- (void)processLinkTagArgs:(NSDictionary *)inArgs attributes:(AITextAttributes *)textAttributes;
David@0
    46
- (NSDictionary *)processSpanTagArgs:(NSDictionary *)inArgs attributes:(AITextAttributes *)textAttributes;
David@0
    47
- (void)processDivTagArgs:(NSDictionary *)inArgs attributes:(AITextAttributes *)textAttributes;
David@0
    48
- (NSAttributedString *)processImgTagArgs:(NSDictionary *)inArgs attributes:(AITextAttributes *)textAttributes baseURL:(NSString *)baseURL;
David@0
    49
- (BOOL)appendImage:(NSImage *)attachmentImage
David@0
    50
			 atPath:(NSString *)inPath
David@0
    51
		   toString:(NSMutableString *)string
David@0
    52
		   withName:(NSString *)inName 
David@0
    53
		 imageClass:(NSString *)imageClass
David@0
    54
		 imagesPath:(NSString *)imagesPath
David@0
    55
	  uniqueifyHTML:(BOOL)uniqueifyHTML;
David@0
    56
David@0
    57
- (void)restoreAttributesFromDict:(NSDictionary *)inAttributes intoAttributes:(AITextAttributes *)textAttributes;
David@0
    58
@end
David@0
    59
David@0
    60
@interface NSString (AIHTMLDecoderAdditions)
David@0
    61
- (NSString *)stringByConvertingWingdingsToUnicode;
David@0
    62
- (NSString *)stringByConvertingSymbolToSymbolUnicode;
David@0
    63
@end
David@0
    64
David@0
    65
@implementation AIHTMLDecoder
David@0
    66
David@0
    67
static NSString			*horizontalRule = nil;
David@0
    68
David@0
    69
+ (void)initialize
David@0
    70
{
David@0
    71
	//Set up the horizontal rule which will be searched-for when encoding and inserted when decoding
David@0
    72
	if ((self == [AIHTMLDecoder class])) {
David@0
    73
#define HORIZONTAL_BAR			0x2013
David@0
    74
#define HORIZONTAL_RULE_LENGTH	12
David@0
    75
		
David@0
    76
		const unichar separatorUTF16[HORIZONTAL_RULE_LENGTH] = {
David@0
    77
			'\n', HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR,
David@0
    78
			HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, '\n'
David@0
    79
		};
David@0
    80
		horizontalRule = [[NSString alloc] initWithCharacters:separatorUTF16 length:HORIZONTAL_RULE_LENGTH];
David@0
    81
	}	
David@0
    82
}
David@0
    83
David@1562
    84
- (void) dealloc {
David@1562
    85
	[XMLNamespace release]; XMLNamespace = nil;
David@1562
    86
	[baseURL release]; baseURL = nil;
David@1562
    87
	[super dealloc];
David@1562
    88
}
David@1562
    89
David@0
    90
+ (AIHTMLDecoder *)decoder
David@0
    91
{
David@0
    92
	return [[[self alloc] init] autorelease];
David@0
    93
}
David@0
    94
David@0
    95
- (id)initWithHeaders:(BOOL)includeHeaders
David@0
    96
			 fontTags:(BOOL)includeFontTags
David@0
    97
		closeFontTags:(BOOL)closeFontTags
David@0
    98
			colorTags:(BOOL)includeColorTags
David@0
    99
			styleTags:(BOOL)includeStyleTags
David@0
   100
	   encodeNonASCII:(BOOL)encodeNonASCII
David@0
   101
		 encodeSpaces:(BOOL)encodeSpaces
David@0
   102
	attachmentsAsText:(BOOL)attachmentsAsText
David@0
   103
onlyIncludeOutgoingImages:(BOOL)onlyIncludeOutgoingImages
David@0
   104
	   simpleTagsOnly:(BOOL)simpleOnly
David@0
   105
	   bodyBackground:(BOOL)bodyBackground
David@0
   106
  allowJavascriptURLs:(BOOL)allowJS
David@0
   107
{
David@0
   108
	if ((self = [self init])) {
David@0
   109
		thingsToInclude.headers							= includeHeaders;
David@0
   110
		thingsToInclude.fontTags						= includeFontTags;
David@0
   111
		thingsToInclude.closingFontTags					= closeFontTags;
David@0
   112
		thingsToInclude.colorTags						= includeColorTags;
David@0
   113
		thingsToInclude.styleTags						= includeStyleTags;
David@0
   114
		thingsToInclude.nonASCII						= encodeNonASCII;
David@0
   115
		thingsToInclude.allSpaces						= encodeSpaces;
David@0
   116
		thingsToInclude.attachmentTextEquivalents		= attachmentsAsText;
David@0
   117
		thingsToInclude.onlyIncludeOutgoingImages	= onlyIncludeOutgoingImages;
David@0
   118
		thingsToInclude.simpleTagsOnly					= simpleOnly;
David@0
   119
		thingsToInclude.bodyBackground					= bodyBackground;
David@0
   120
		thingsToInclude.allowJavascriptURLs				= allowJS;
David@0
   121
		
David@0
   122
		thingsToInclude.allowAIMsubprofileLinks			= NO;
David@0
   123
	}
David@0
   124
	
David@0
   125
	return self;
David@0
   126
}
David@0
   127
David@0
   128
+ (AIHTMLDecoder *)decoderWithHeaders:(BOOL)includeHeaders
David@0
   129
							 fontTags:(BOOL)includeFontTags
David@0
   130
						closeFontTags:(BOOL)closeFontTags
David@0
   131
							colorTags:(BOOL)includeColorTags
David@0
   132
							styleTags:(BOOL)includeStyleTags
David@0
   133
					   encodeNonASCII:(BOOL)encodeNonASCII
David@0
   134
						 encodeSpaces:(BOOL)encodeSpaces
David@0
   135
					attachmentsAsText:(BOOL)attachmentsAsText
David@0
   136
			onlyIncludeOutgoingImages:(BOOL)onlyIncludeOutgoingImages
David@0
   137
					   simpleTagsOnly:(BOOL)simpleOnly
David@0
   138
					   bodyBackground:(BOOL)bodyBackground
David@0
   139
                  allowJavascriptURLs:(BOOL)allowJS
David@0
   140
{
David@0
   141
	return [[[self alloc] initWithHeaders:includeHeaders
David@0
   142
								 fontTags:includeFontTags
David@0
   143
							closeFontTags:closeFontTags
David@0
   144
								colorTags:includeColorTags
David@0
   145
								styleTags:includeStyleTags
David@0
   146
						   encodeNonASCII:encodeNonASCII
David@0
   147
							 encodeSpaces:encodeSpaces
David@0
   148
						attachmentsAsText:attachmentsAsText
David@0
   149
		   onlyIncludeOutgoingImages:onlyIncludeOutgoingImages
David@0
   150
						   simpleTagsOnly:simpleOnly
David@0
   151
						   bodyBackground:bodyBackground
David@0
   152
					  allowJavascriptURLs:allowJS] autorelease];
David@0
   153
}
David@0
   154
David@0
   155
#pragma mark Work methods
David@0
   156
David@0
   157
/*!
David@0
   158
 * @brief Parse arguments in a string
David@0
   159
 *
David@0
   160
 * The arguments are returned in an NSDictionary whose keys are all-lowercase
David@0
   161
 */
David@0
   162
- (NSDictionary *)parseArguments:(NSString *)arguments
David@0
   163
{
David@0
   164
	NSMutableDictionary		*argDict;
David@0
   165
	NSScanner				*scanner;
David@0
   166
	static NSCharacterSet	*equalsSet = nil,
David@0
   167
		*dquoteSet = nil,
David@0
   168
		*squoteSet = nil,
David@0
   169
		*spaceSet = nil;
David@0
   170
	NSString				*key = nil, *value = nil;
David@0
   171
David@0
   172
	//Setup
David@0
   173
	if (!equalsSet) equalsSet = [[NSCharacterSet characterSetWithCharactersInString:@"="]  retain];
David@0
   174
	if (!dquoteSet) dquoteSet = [[NSCharacterSet characterSetWithCharactersInString:@"\""] retain];
David@0
   175
	if (!squoteSet) squoteSet = [[NSCharacterSet characterSetWithCharactersInString:@"'"]  retain];
David@0
   176
	if (!spaceSet)  spaceSet  = [[NSCharacterSet characterSetWithCharactersInString:@" "]  retain];
David@0
   177
David@0
   178
	scanner = [NSScanner scannerWithString:arguments];
David@0
   179
	argDict = [NSMutableDictionary dictionary];
David@0
   180
David@0
   181
	while (![scanner isAtEnd]) {
David@0
   182
		BOOL	validKey, validValue;
David@0
   183
David@0
   184
		//Find a tag
David@0
   185
		validKey = [scanner scanUpToCharactersFromSet:equalsSet intoString:&key];
David@0
   186
		[scanner scanCharactersFromSet:equalsSet intoString:nil];
David@0
   187
David@0
   188
		//check for quotes
David@0
   189
		if ([scanner scanCharactersFromSet:dquoteSet intoString:nil]) {
David@0
   190
			validValue = [scanner scanUpToCharactersFromSet:dquoteSet intoString:&value];
David@0
   191
			[scanner scanCharactersFromSet:dquoteSet intoString:nil];
David@0
   192
		} else if ([scanner scanCharactersFromSet:squoteSet intoString:nil]) {
David@0
   193
			validValue = [scanner scanUpToCharactersFromSet:squoteSet intoString:&value];
David@0
   194
			[scanner scanCharactersFromSet:squoteSet intoString:nil];
David@0
   195
		} else {
David@0
   196
			validValue = [scanner scanUpToCharactersFromSet:spaceSet intoString:&value];
David@0
   197
		}
David@0
   198
David@0
   199
		//Store in dict
David@0
   200
		if (validValue && value != nil && [value length] != 0 && validKey && key != nil && [key length] != 0) { //Watch out for invalid & empty tags
David@0
   201
			[argDict setObject:value forKey:[key lowercaseString]];
David@0
   202
		}
David@0
   203
	}
David@0
   204
David@0
   205
	return argDict;
David@0
   206
}
David@0
   207
David@0
   208
- (NSString *)encodeLooseHTML:(NSAttributedString *)inMessage imagesPath:(NSString *)imagesSavePath
David@0
   209
{
David@0
   210
	NSFontManager	*fontManager = [NSFontManager sharedFontManager];
David@0
   211
	NSRange			 searchRange;
David@0
   212
	NSColor			*pageColor = nil;
David@0
   213
	BOOL			 openFontTag = NO;
David@0
   214
David@0
   215
	//Setup the incoming message as a regular string, and get its length
David@0
   216
	NSString		*inMessageString = [inMessage string];
David@0
   217
	unsigned		 messageLength = [inMessageString length];
David@0
   218
	
David@0
   219
	//Setup the destination HTML string
David@0
   220
	NSMutableString *string = [NSMutableString string];
David@0
   221
	if (thingsToInclude.headers) {
David@0
   222
		[string appendString:@"<HTML>"];
David@0
   223
	}
David@0
   224
David@0
   225
	//If the text is right-to-left, enclose all our HTML in an rtl DIV tag
David@0
   226
	BOOL	rightToLeft = NO;
David@0
   227
	if (!thingsToInclude.simpleTagsOnly) {
David@0
   228
		if (messageLength > 0) {
David@0
   229
			//First, attempt to figure the base writing direction of our message based on its content
David@0
   230
			NSWritingDirection	dir = [inMessageString baseWritingDirection];
David@0
   231
David@0
   232
			//If that doesn't work, try using the writing direction of the input field
David@0
   233
			if (dir == NSWritingDirectionNatural) {
David@0
   234
				dir = [[inMessage attribute:NSParagraphStyleAttributeName
David@0
   235
									atIndex:0
David@0
   236
							 effectiveRange:nil] baseWritingDirection];
David@0
   237
				
David@0
   238
				//If the input field's writing direction is NSWritingDirectionNatural, we shall figure what it really means.
David@0
   239
				//The natural writing direction is determined by the system based on the current active localization of the app.
David@0
   240
				if (dir == NSWritingDirectionNatural)
David@0
   241
					dir = [NSParagraphStyle defaultWritingDirectionForLanguage:nil];
David@0
   242
			}
David@0
   243
			
David@0
   244
			if (dir == NSWritingDirectionRightToLeft) {
David@0
   245
				[string appendString:@"<DIV dir=\"rtl\">"];
David@0
   246
				rightToLeft = YES;
David@0
   247
			}
David@0
   248
		}
David@0
   249
	}	
David@0
   250
	
David@0
   251
	//Setup the default attributes
zacw@2667
   252
	NSString		*currentFamily = [@"Helvetica" retain];
David@0
   253
	NSString		*currentColor = nil;
David@0
   254
	NSString		*currentBackColor = nil;
zacw@2667
   255
	int				 currentSize = 12;
David@0
   256
	BOOL			 currentItalic = NO;
David@0
   257
	BOOL			 currentBold = NO;
David@0
   258
	BOOL			 currentUnderline = NO;
David@0
   259
	BOOL			 currentStrikethrough = NO;
David@0
   260
	NSString		*link = nil;
David@0
   261
	NSString		*oldLink = nil;
David@0
   262
	
David@0
   263
	//Append the body tag (If there is a background color)
David@0
   264
	if (thingsToInclude.headers &&
David@0
   265
	   (messageLength > 0) &&
David@0
   266
	   (pageColor = [inMessage attribute:AIBodyColorAttributeName
David@0
   267
								 atIndex:0
David@0
   268
						  effectiveRange:nil])) {
David@0
   269
		[string appendString:@"<BODY BGCOLOR=\"#"];
David@0
   270
		[string appendString:[pageColor hexString]];
David@0
   271
		[string appendString:@"\">"];
David@0
   272
	}
David@0
   273
David@0
   274
	//Loop through the entire string
David@0
   275
	searchRange = NSMakeRange(0,0);
David@0
   276
	while (searchRange.location < messageLength) {
David@0
   277
		NSDictionary	*attributes = [inMessage attributesAtIndex:searchRange.location effectiveRange:&searchRange];
David@0
   278
		NSFont			*font = [attributes objectForKey:NSFontAttributeName];
David@0
   279
		NSString		*color = (thingsToInclude.colorTags ? [[attributes objectForKey:NSForegroundColorAttributeName] hexString] : nil);
David@0
   280
		NSString		*backColor = (thingsToInclude.colorTags ? [[attributes objectForKey:NSBackgroundColorAttributeName] hexString] : nil);
David@0
   281
		NSString		*familyName = [font familyName];
David@0
   282
		float			 pointSize = [font pointSize];
David@0
   283
David@0
   284
		NSFontTraitMask	 traits = [fontManager traitsOfFont:font];
David@0
   285
		BOOL			 hasUnderline = [[attributes objectForKey:NSUnderlineStyleAttributeName] intValue];
David@0
   286
		BOOL			 hasStrikethrough = [[attributes objectForKey:NSStrikethroughStyleAttributeName] intValue];
David@0
   287
		BOOL			 isBold = (traits & NSBoldFontMask);
David@0
   288
		BOOL			 isItalic = (traits & NSItalicFontMask);
David@0
   289
		
David@0
   290
		link = [[attributes objectForKey:NSLinkAttributeName] absoluteString];
David@0
   291
		
David@0
   292
		//If we had a link on the last pass, and we don't now or we have a different one, close the link tag
David@0
   293
		if (oldLink &&
David@0
   294
			(!link || (([link length] != 0) && ![oldLink isEqualToString:link]))) {
David@0
   295
David@0
   296
			//Close Link
David@0
   297
			[string appendString:@"</a>"];
David@0
   298
			oldLink = nil;
David@0
   299
		}
David@0
   300
		
David@0
   301
		NSMutableString	*chunk = [[inMessageString substringWithRange:searchRange] mutableCopy];
David@0
   302
David@0
   303
		//Font (If the color, font, or size has changed)
David@0
   304
		BOOL changedSize = (pointSize != currentSize);
David@0
   305
		BOOL changedColor = ((color && ![color isEqualToString:currentColor]) || (!color && currentColor));
David@0
   306
		BOOL changedBackColor = ((backColor && ![backColor isEqualToString:currentBackColor]) || (!backColor && currentBackColor));
David@0
   307
		if((thingsToInclude.fontTags && (changedSize || ![familyName isEqualToString:currentFamily])) ||
David@0
   308
		   changedColor || changedBackColor) {
David@0
   309
David@0
   310
			//Close any existing font tags, and open a new one
David@0
   311
			if (thingsToInclude.closingFontTags && openFontTag) {
David@0
   312
				[string appendString:@"</FONT>"];
David@0
   313
			}
David@0
   314
			if (!thingsToInclude.simpleTagsOnly) {
David@0
   315
				openFontTag = YES;
David@0
   316
				//Leave the <FONT open since we'll add the rest of the font tag on below
David@0
   317
				[string appendString:@"<FONT"];
David@0
   318
			}
David@0
   319
David@0
   320
			//Family
David@0
   321
			if (thingsToInclude.fontTags && familyName && (![familyName isEqualToString:currentFamily] || thingsToInclude.closingFontTags)) {
David@0
   322
				if (thingsToInclude.simpleTagsOnly) {
David@0
   323
					[string appendString:[NSString stringWithFormat:@"<FONT FACE=\"%@\">",familyName]];
David@0
   324
				} else {
David@0
   325
					//(traits | NSNonStandardCharacterSetFontMask) seems to be the proper test... but it is true for all fonts!
David@0
   326
					//NSMacOSRomanStringEncoding seems to be the encoding of all standard Roman fonts... and langNum="11" seems to make the others send properly.
David@0
   327
					//It serves us well here.  Once non-AIM HTML is coming through, this will probably need to be an option in the function call.
David@0
   328
					if ([font mostCompatibleStringEncoding] != NSMacOSRomanStringEncoding) {
David@0
   329
						[string appendString:[NSString stringWithFormat:@" FACE=\"%@\" LANG=\"11\"",familyName]];
David@0
   330
					} else {
David@0
   331
						[string appendString:[NSString stringWithFormat:@" FACE=\"%@\"",familyName]];
David@0
   332
					}
David@0
   333
David@0
   334
				}
David@0
   335
				[currentFamily release]; currentFamily = [familyName retain];
David@0
   336
			}
David@0
   337
David@0
   338
			//Size
David@0
   339
			if (thingsToInclude.fontTags && font && !thingsToInclude.simpleTagsOnly) {
David@0
   340
				[string appendString:[NSString stringWithFormat:@" ABSZ=%i SIZE=%i", (int)pointSize, HTMLEquivalentForFontSize((int)pointSize)]];
David@0
   341
				currentSize = pointSize;
David@0
   342
			}
David@0
   343
David@0
   344
			//Color
David@0
   345
			if (color) {
David@0
   346
				if (thingsToInclude.simpleTagsOnly) {
David@0
   347
					[string appendString:[NSString stringWithFormat:@"<FONT COLOR=\"#%@\">",color]];	
David@0
   348
				} else {
David@0
   349
					[string appendString:[NSString stringWithFormat:@" COLOR=\"#%@\"",color]];
David@0
   350
				}
David@0
   351
			}
David@0
   352
			//Background Color per tag
David@0
   353
			if (backColor) {
David@0
   354
				if (!thingsToInclude.simpleTagsOnly) {	
David@0
   355
					[string appendString:[NSString stringWithFormat:@" BACK=\"#%@\"",backColor]];
David@0
   356
				}
David@0
   357
			}
David@0
   358
			
David@0
   359
			if (color != currentColor) {
catfish@2229
   360
				currentColor = color;
David@0
   361
			}
David@0
   362
			
David@0
   363
			if (backColor != currentBackColor) {
catfish@2229
   364
				currentBackColor = backColor;
David@0
   365
			}
David@0
   366
David@0
   367
			//Close the font tag if necessary
David@0
   368
			if (!thingsToInclude.simpleTagsOnly) {
David@0
   369
				[string appendString:@">"];
David@0
   370
			}
David@0
   371
		}
David@0
   372
David@0
   373
		//Style (Bold, italic, underline, strikethrough)
David@0
   374
		if (thingsToInclude.styleTags) {			
David@0
   375
			if (currentItalic && !isItalic) {
David@0
   376
				[string appendString:@"</I>"];
David@0
   377
				currentItalic = NO;
David@0
   378
			} else  if (!currentItalic && isItalic) {
David@0
   379
				[string appendString:@"<I>"];
David@0
   380
				currentItalic = YES;
David@0
   381
			}
David@0
   382
David@0
   383
			if (currentUnderline && !hasUnderline) {
David@0
   384
				[string appendString:@"</U>"];
David@0
   385
				currentUnderline = NO;
David@0
   386
			} else if (!currentUnderline && hasUnderline) {
David@0
   387
				[string appendString:@"<U>"];
David@0
   388
				currentUnderline = YES;
David@0
   389
			}
David@0
   390
David@0
   391
			if (currentBold && !isBold) {
David@0
   392
				[string appendString:@"</B>"];
David@0
   393
				currentBold = NO;
David@0
   394
			} else if (!currentBold && isBold) {
David@0
   395
				[string appendString:@"<B>"];
David@0
   396
				currentBold = YES;
David@0
   397
			}
David@0
   398
        
David@0
   399
			if (currentStrikethrough && !hasStrikethrough) {
David@0
   400
				[string appendString:@"</S>"];
David@0
   401
				currentStrikethrough = NO;
David@0
   402
			} else if (!currentStrikethrough && hasStrikethrough) {
David@0
   403
				[string appendString:@"<S>"];
David@0
   404
				currentStrikethrough = YES;
David@0
   405
			}
David@0
   406
		}
David@0
   407
David@0
   408
		//Link
David@0
   409
		if (!oldLink && link && [link length] != 0) {
David@0
   410
			NSString	*linkString = ([link isKindOfClass:[NSURL class]] ? [(NSURL *)link absoluteString] : link);
David@0
   411
David@0
   412
			/* For incoming messages, javascript urls are both dangerous and useless.
David@0
   413
			 * If thingsToInclude.allowJavascriptURLs is NO, we refuse to create <a> tags for links starting with javascript.
David@0
   414
			 * This probably should be set to NO for outgoing messages, to avoid MSN-style-censorship accusations.
David@0
   415
			 */
David@0
   416
			if (thingsToInclude.allowJavascriptURLs || [linkString rangeOfString:@"javascript"].location > 0) {
David@0
   417
			
David@0
   418
				[string appendString:@"<a href=\""];
David@0
   419
				
David@0
   420
				/* AIM can handle %n in links, which is highly invalid for a real URL.
David@0
   421
				 * If thingsToInclude.allowAIMsubprofileLinks is YES, and a %25n is in the link, replace the escaped version
David@0
   422
				 * which was used within Adium [so that NSURL didn't balk] with %n, which is what other AIM clients will
David@0
   423
				 * be expecting.
David@0
   424
				 */
David@0
   425
				if (thingsToInclude.allowAIMsubprofileLinks && 
David@0
   426
				   ([linkString rangeOfString:@"%25n"].location != NSNotFound)) {
David@0
   427
					NSMutableString	*fixedLinkString = [[linkString mutableCopy] autorelease];
David@0
   428
					[fixedLinkString replaceOccurrencesOfString:@"%25n"
David@0
   429
													 withString:@"%n"
David@0
   430
														options:NSLiteralSearch
David@0
   431
														  range:NSMakeRange(0, [fixedLinkString length])];
David@0
   432
					linkString = fixedLinkString;
David@0
   433
				}
David@0
   434
				
terminus@2578
   435
				[string appendString:[linkString stringByEscapingForXMLWithEntities:nil]];
David@0
   436
				if (!thingsToInclude.simpleTagsOnly) {
David@0
   437
					[string appendString:@"\" title=\""];
terminus@2578
   438
					[string appendString:[linkString stringByEscapingForXMLWithEntities:nil]];
David@0
   439
				}
zacw@2276
   440
				
zacw@2276
   441
				NSString *classString = [attributes objectForKey:AIElementClassAttributeName];
zacw@2276
   442
				
zacw@2276
   443
				if (!thingsToInclude.simpleTagsOnly && classString) {
zacw@2276
   444
					[string appendString:@"\" class=\""];
zacw@2276
   445
					[string appendString:classString];
zacw@2276
   446
				}
zacw@2276
   447
				
David@0
   448
				[string appendString:@"\">"];
David@0
   449
				
David@0
   450
				oldLink = linkString;
David@0
   451
				
David@0
   452
			}
David@0
   453
		}
David@0
   454
David@0
   455
		//Image Attachments
David@0
   456
		if ([attributes objectForKey:NSAttachmentAttributeName]) {
David@0
   457
David@0
   458
			//Each attachment takes a character.. they are grouped by the attribute scan
David@0
   459
			for (int i = 0; (i < searchRange.length); i++) { 
David@0
   460
				NSTextAttachment *textAttachment = [[inMessage attributesAtIndex:searchRange.location+i 
David@0
   461
																  effectiveRange:nil] objectForKey:NSAttachmentAttributeName];
David@0
   462
David@0
   463
				if (textAttachment) {
David@0
   464
					if (![textAttachment isKindOfClass:[AITextAttachmentExtension class]]) {
David@0
   465
						NSLog(@"Message %@ gave an NSTextAttachment %@ - why is it not an AITextAttachmentExtension?",
David@0
   466
							  inMessage,
David@0
   467
							  textAttachment);
David@0
   468
						continue;
David@0
   469
					}
David@0
   470
					AITextAttachmentExtension *attachment = (AITextAttachmentExtension *)textAttachment;
David@0
   471
					/* If we have a path to which we want to save any images and either
David@0
   472
					 *		the attachment should save such images OR
David@0
   473
					 *		the attachment is a plain NSTextAttachment and so doesn't respond to shouldSaveImageForLogging
David@0
   474
					 *
David@0
   475
					 * If we should save the image, we'll also tell the appendImage method to uniquify the HTML so it'll load
David@0
   476
					 * from disk each time it's displayed, preventing a WebView from caching it.
David@0
   477
					 */						
David@0
   478
					BOOL shouldSaveImage = (imagesSavePath &&
David@0
   479
											((![attachment respondsToSelector:@selector(shouldSaveImageForLogging)] ||
David@0
   480
											  [attachment shouldSaveImageForLogging])));
David@0
   481
					/* We want attachments as images where appropriate. We either want all images (we don't want only outgoing images) or
David@0
   482
					 * this attachment may be sent as an image rather than as text.
David@0
   483
					 */						
David@0
   484
					BOOL shouldIncludeImageWithoutSaving = (!thingsToInclude.attachmentTextEquivalents &&
David@0
   485
															(!thingsToInclude.onlyIncludeOutgoingImages || (![attachment respondsToSelector:@selector(shouldAlwaysSendAsText)] ||
David@0
   486
																											![attachment shouldAlwaysSendAsText])));
David@0
   487
					BOOL appendedImage = NO;
David@0
   488
David@0
   489
					if (shouldSaveImage || shouldIncludeImageWithoutSaving) {
David@0
   490
						NSString	*existingPath, *imageName;
David@0
   491
						NSImage		*image;
David@0
   492
						
David@0
   493
						//We want to use the image; collect all the information we have available
David@0
   494
						existingPath = ([attachment respondsToSelector:@selector(path)] ?
David@0
   495
										[attachment performSelector:@selector(path)] :
David@0
   496
										nil);
David@0
   497
						imageName = [attachment string];
David@0
   498
						
David@0
   499
						image = nil;
David@0
   500
David@0
   501
						/*
David@0
   502
							Although PDFs are treated as images on OSX, they can cause issues on other platforms.
David@0
   503
							Also, moreso than most other images, they can be too large to display inline.
David@0
   504
						 */
David@0
   505
						if(!existingPath || ![[existingPath pathExtension] isEqualToString:@"pdf"])
David@0
   506
						{
David@0
   507
							if ([attachment respondsToSelector:@selector(image)])
David@0
   508
								image = [attachment performSelector:@selector(image)];
David@0
   509
							else if ([[attachment attachmentCell] respondsToSelector:@selector(image)])
David@0
   510
								image = [[attachment attachmentCell] performSelector:@selector(image)];
David@0
   511
						}
David@0
   512
David@0
   513
						if (existingPath || image) {
David@0
   514
							appendedImage = [self appendImage:image
David@0
   515
													   atPath:existingPath
David@0
   516
													 toString:string
David@0
   517
													 withName:imageName
David@0
   518
												   imageClass:[attachment imageClass]
David@0
   519
												   imagesPath:(shouldIncludeImageWithoutSaving ? imagesSavePath : nil)
David@0
   520
												uniqueifyHTML:shouldSaveImage];
David@0
   521
							
David@0
   522
							//We were succesful appending the image tag, so release this chunk
David@0
   523
							[chunk release]; chunk = nil;	
David@0
   524
						}
David@0
   525
					}
David@0
   526
					
David@0
   527
					if (!appendedImage) {
David@0
   528
						//We should replace the attachment with its textual equivalent if we didn't append an image
David@0
   529
						NSString	*attachmentString;
David@0
   530
						if ((attachmentString = [attachment string])) {
David@0
   531
							[string appendString:attachmentString];
David@0
   532
						}
David@0
   533
						
David@0
   534
						[chunk release]; chunk = nil;
David@0
   535
					}
David@0
   536
				}
David@0
   537
			}
David@0
   538
		}
David@0
   539
David@0
   540
		if (chunk) {
David@0
   541
			NSRange	fullRange;
David@0
   542
			unsigned int replacements;
David@0
   543
David@0
   544
			//Escape special HTML characters.
David@0
   545
			fullRange = NSMakeRange(0, [chunk length]);
David@0
   546
David@0
   547
			replacements = [chunk replaceOccurrencesOfString:@"&" withString:@"&amp;"
David@0
   548
													 options:NSLiteralSearch range:fullRange];
David@0
   549
			fullRange.length += (replacements * 4);
David@0
   550
				
David@0
   551
			replacements = [chunk replaceOccurrencesOfString:@"<" withString:@"&lt;"
David@0
   552
									  options:NSLiteralSearch range:fullRange];
David@0
   553
			fullRange.length += (replacements * 3);
David@0
   554
			
David@0
   555
			replacements = [chunk replaceOccurrencesOfString:@">" withString:@"&gt;"
David@0
   556
													 options:NSLiteralSearch range:fullRange];
David@0
   557
			fullRange.length += (replacements * 3);
David@0
   558
David@0
   559
			//Horizontal rule
David@0
   560
			replacements = [chunk replaceOccurrencesOfString:horizontalRule withString:@"<HR>"
David@0
   561
													 options:NSLiteralSearch range:fullRange];
David@0
   562
			if (replacements) {
David@0
   563
				fullRange.length = [chunk length];
David@0
   564
			}
David@0
   565
David@0
   566
			if (thingsToInclude.allSpaces) {
David@0
   567
				/* Replace the tabs first, if they exist, so that it creates a leading " " when the tab is the initial character, and 
David@0
   568
				 * so subsequent tab formatting is preserved.
David@0
   569
				 */
David@0
   570
				replacements = [chunk replaceOccurrencesOfString:@"\t" 
David@0
   571
													  withString:@" &nbsp;&nbsp;&nbsp;"
David@0
   572
														 options:NSLiteralSearch
David@0
   573
														   range:fullRange];
David@0
   574
				fullRange.length += (replacements * 18);
David@0
   575
David@0
   576
				//If the first character is a space, replace that leading ' ' with "&nbsp;" to preserve formatting.
David@1037
   577
				if ([chunk hasPrefix:@" "]) {
David@0
   578
					[chunk replaceCharactersInRange:NSMakeRange(0, 1)
David@0
   579
										 withString:@"&nbsp;"];
David@0
   580
					fullRange.length += 5;
David@0
   581
				}
David@0
   582
	
David@0
   583
				/* Replace all remaining blocks of "  " (<space><space>) with " &nbsp;" (<space><&nbsp;>) so that
David@0
   584
				 * formatting of large blocks of spaces in the middle of a line is preserved,
David@0
   585
				 * and so WebKit properly line-wraps.
David@0
   586
				 */
David@0
   587
				[chunk replaceOccurrencesOfString:@"  "
David@0
   588
									   withString:@" &nbsp;"
David@0
   589
										  options:NSLiteralSearch
David@0
   590
											range:fullRange];
David@0
   591
			}
David@0
   592
David@0
   593
			/* Encode line endings */
David@0
   594
			
David@0
   595
			//Handle \r\n as a single line break
David@0
   596
			[chunk replaceOccurrencesOfString:@"\r\n"
David@0
   597
								   withString:@"<BR>"
David@0
   598
									  options:NSLiteralSearch 
David@0
   599
										range:fullRange];
David@0
   600
			
David@0
   601
			//Now replace every remaining line ending
David@0
   602
			NSRange lineEndingRange;
David@0
   603
			while ((lineEndingRange = [chunk rangeOfLineBreakCharacter]).location != NSNotFound) {
David@0
   604
				[chunk replaceCharactersInRange:lineEndingRange
David@0
   605
									 withString:@"<BR>"];
David@0
   606
			}
David@0
   607
David@0
   608
			/* If we need to encode non-ASCII to HTML, append string character by
David@0
   609
			 * character, replacing any non-ascii characters with the designated SGML escape sequence.
David@0
   610
			 */
David@0
   611
			if (thingsToInclude.nonASCII) {
David@0
   612
				unsigned length = [chunk length];
David@0
   613
				for (unsigned i = 0; i < length; i++) {
David@0
   614
					unichar currentChar = [chunk characterAtIndex:i];
David@0
   615
					if (currentChar < 32) {
David@0
   616
						//Control character.
David@0
   617
						[string appendFormat:@"&#x%x;", currentChar];
David@0
   618
David@0
   619
					} else if (currentChar >= 127) {
David@0
   620
						if (!UCIsSurrogateHighCharacter(currentChar)) {
David@0
   621
							[string appendFormat:@"&#x%x;", currentChar];
David@0
   622
David@0
   623
						} else {
David@0
   624
							//currentChar is the high character of a surrogate pair.
David@0
   625
							unichar lowSurrogate = 0xFFFF;
David@0
   626
							if ((i + 1) < length) {
David@0
   627
								lowSurrogate = [chunk characterAtIndex:++i];
David@0
   628
							}
David@0
   629
David@0
   630
							if (!UCIsSurrogateLowCharacter(lowSurrogate)) {
David@0
   631
								//In case you're wondering: 0xFFFF is not a low surrogate. (Nor anything else, for that matter.)
David@0
   632
								AILog(@"AIHTMLDecoder: Got high surrogate of surrogate pair, but there's no low surrogate after it. This is at index %u of chunk with length %u. The chunk is: %@", i, length, chunk);
David@0
   633
David@0
   634
							} else {
David@0
   635
								UnicodeScalarValue codePoint = UCGetUnicodeScalarValueForSurrogatePair(/*highSurrogate*/ currentChar, lowSurrogate);
David@0
   636
								[string appendFormat:@"&#x%x;", codePoint];
David@0
   637
							}
David@0
   638
						}
David@0
   639
David@0
   640
					} else {
David@0
   641
						//unichar characters may have a length of up to 3; be careful to get the whole character
David@0
   642
						NSRange composedCharRange = [chunk rangeOfComposedCharacterSequenceAtIndex:i];
David@0
   643
						[string appendString:[chunk substringWithRange:composedCharRange]];
David@0
   644
						i += composedCharRange.length - 1;
David@0
   645
					}
David@0
   646
				}
David@0
   647
David@0
   648
			} else {
David@0
   649
				[string appendString:chunk];
David@0
   650
			}
David@0
   651
David@0
   652
			//Release the chunk
David@0
   653
			[chunk release];
David@0
   654
		}
David@0
   655
David@0
   656
		searchRange.location += searchRange.length;
David@0
   657
	}
David@0
   658
David@0
   659
	[currentFamily release];
David@0
   660
David@0
   661
	//Finish off the HTML
David@0
   662
	if (thingsToInclude.styleTags) {
David@0
   663
		if (currentItalic)        [string appendString:@"</I>"];
David@0
   664
		if (currentBold)          [string appendString:@"</B>"];
David@0
   665
		if (currentUnderline)     [string appendString:@"</U>"];
David@0
   666
		if (currentStrikethrough) [string appendString:@"</S>"];
David@0
   667
	}
David@0
   668
	
David@0
   669
	//If we had a link on the last pass, close the link tag
David@0
   670
	if (oldLink) {
David@0
   671
		//Close Link
David@0
   672
		[string appendString:@"</a>"];
David@0
   673
		oldLink = nil;
David@0
   674
	}
David@0
   675
David@0
   676
	if (thingsToInclude.fontTags && thingsToInclude.closingFontTags && openFontTag) {
David@0
   677
		//Close any open font tag
David@0
   678
		[string appendString:@"</FONT>"];
David@0
   679
	}
David@0
   680
	if (rightToLeft) {
David@0
   681
		//Close any open div
David@0
   682
		[string appendString:@"</DIV>"];
David@0
   683
	}
David@0
   684
	if (thingsToInclude.headers && pageColor) {
David@0
   685
		//Close the body tag
David@0
   686
		[string appendString:@"</BODY>"];
David@0
   687
	}
David@0
   688
	if (thingsToInclude.headers) {
David@0
   689
		//Close the HTML
David@0
   690
		[string appendString:@"</HTML>"];
David@0
   691
	}
David@0
   692
	
David@0
   693
	//KBOTC's odd hackish body background thingy for WMV since no one else will add it
David@0
   694
	if (thingsToInclude.bodyBackground &&
David@0
   695
	   (messageLength > 0)) {
David@0
   696
		[string setString:@""];
David@0
   697
		if ((pageColor = [inMessage attribute:AIBodyColorAttributeName atIndex:0 effectiveRange:nil])) {
David@0
   698
			[string setString:[pageColor hexString]];
David@0
   699
			[string replaceOccurrencesOfString:@"\"" 
David@0
   700
									withString:@"" 
David@0
   701
									   options:NSLiteralSearch
David@0
   702
										 range:NSMakeRange(0, [string length])];
David@0
   703
		}
David@0
   704
	}
David@0
   705
David@0
   706
	return string;
David@0
   707
}
David@0
   708
zacw@1226
   709
- (AIXMLElement *)elementWithAppKitAttributes:(NSDictionary *)attributes
David@0
   710
							   attributeNames:(NSSet *)attributeNames
David@0
   711
							   elementContent:(NSMutableString *)elementContent
David@0
   712
		  shouldAddElementContentToTopElement:(out BOOL *)outAddElementContentToTopElement
David@0
   713
								   imagesPath:(NSString *)imagesPath
David@0
   714
{
David@0
   715
	if (!(attributes && [attributes count] && attributeNames && [attributeNames count]))
David@0
   716
		return nil;
David@0
   717
David@0
   718
	attributes = [attributes dictionaryWithIntersectionWithSetOfKeys:attributeNames];
David@0
   719
David@0
   720
	NSString         *linkValue       = [attributes objectForKey:NSLinkAttributeName];
David@0
   721
	NSTextAttachment *attachmentValue = [attributes objectForKey:NSAttachmentAttributeName];
David@0
   722
David@0
   723
	NSString *elementName = linkValue ? @"a" : @"span";
David@0
   724
	BOOL moreThanJustAnImage = [attributes count] - (attachmentValue != nil);
David@0
   725
David@0
   726
	BOOL addElementContentToTopElement = YES;
zacw@1226
   727
	AIXMLElement *thisElement = moreThanJustAnImage ? [AIXMLElement elementWithNamespaceName:XMLNamespace elementName:elementName] : nil;
David@0
   728
	if (linkValue) {
zacw@1226
   729
		[thisElement setValue:linkValue forAttribute:@"href"];
David@0
   730
	}
David@0
   731
David@0
   732
	if (attachmentValue) {
Evan@271
   733
		AITextAttachmentExtension *extension;
Evan@271
   734
		if ([attachmentValue isKindOfClass:[AITextAttachmentExtension class]])
Evan@271
   735
			extension = (AITextAttachmentExtension *)attachmentValue;
Evan@271
   736
		else
Evan@271
   737
			extension = [AITextAttachmentExtension textAttachmentExtensionFromTextAttachment:attachmentValue];
Evan@271
   738
David@0
   739
		if ((thingsToInclude.attachmentTextEquivalents ||
David@0
   740
			 !imagesPath ||
David@0
   741
			([extension respondsToSelector:@selector(shouldAlwaysSendAsText)] && [extension shouldAlwaysSendAsText])) &&
David@0
   742
			([extension respondsToSelector:@selector(string)])) {
David@0
   743
			[elementContent setString:[extension string]];
David@0
   744
		} else {
David@0
   745
			/* We have an image we want to save if possible, and we have an imagesPath */
zacw@1226
   746
			AIXMLElement *imageElement = [AIXMLElement elementWithNamespaceName:XMLNamespace elementName:@"img"];
zacw@1226
   747
			[imageElement setSelfCloses:YES];
David@0
   748
David@0
   749
			NSTextAttachmentCell *cell = (NSTextAttachmentCell *)[attachmentValue attachmentCell];
David@0
   750
			NSSize size = [cell cellSize];
zacw@1226
   751
			[imageElement setValue:[[NSNumber numberWithFloat:size.width] stringValue] forAttribute:@"width"];
zacw@1226
   752
			[imageElement setValue:[[NSNumber numberWithFloat:size.height] stringValue] forAttribute:@"height"];
David@0
   753
David@0
   754
			NSString *path = [extension path];
David@0
   755
			if (path) {
David@0
   756
				NSString *destinationPath = [imagesPath stringByAppendingPathComponent:[path lastPathComponent]];
David@474
   757
				if ([[NSFileManager defaultManager] copyItemAtPath:path
David@0
   758
													  toPath:destinationPath
David@474
   759
													 error:NULL]) {
David@0
   760
					/* Just the file name; the XML should be set to have a base URL of the imagesPath */
David@0
   761
					/* It might be good to make this an optional behavior, with the other choice of an absolute
David@0
   762
					 * file URL (destinationPath).
David@0
   763
					 */
zacw@1226
   764
					[imageElement setValue:[path lastPathComponent]
zacw@1226
   765
							  forAttribute:@"src"];					
David@0
   766
				} else {
David@0
   767
					AILogWithSignature(@"Could not copy %@ to %@", path, destinationPath);
David@0
   768
				}
David@0
   769
			}
David@0
   770
David@0
   771
			if (elementContent && [elementContent length]) {
zacw@1226
   772
				[imageElement setValue:elementContent forAttribute:@"alt"];
David@0
   773
			}
David@0
   774
David@0
   775
			if (thisElement) {
zacw@1226
   776
				[thisElement addObject:imageElement];
David@0
   777
			} else {
David@0
   778
				thisElement = imageElement;
David@0
   779
			}
David@0
   780
David@0
   781
			addElementContentToTopElement = NO;
David@0
   782
		}
David@0
   783
	}
David@0
   784
David@0
   785
	NSString *CSSString = [NSAttributedString CSSStringForTextAttributes:attributes];
David@0
   786
	if (CSSString && [CSSString length]) {
zacw@1226
   787
		[thisElement setValue:CSSString forAttribute:@"style"];
David@0
   788
	}
David@0
   789
David@0
   790
	if (outAddElementContentToTopElement) {
David@0
   791
		*outAddElementContentToTopElement = addElementContentToTopElement;
David@0
   792
	}
David@0
   793
David@0
   794
	return thisElement;
David@0
   795
}
David@0
   796
- (NSDictionary *)attributesByReplacingNSFontAttributeNameWithAIFontAttributeNames:(NSDictionary *)attributes
David@0
   797
{
David@0
   798
	NSFont *font = [[attributes objectForKey:NSFontAttributeName] retain];
David@0
   799
	if (!font) {
David@0
   800
		return [[attributes retain] autorelease];
David@0
   801
	} else {
David@0
   802
		NSMutableDictionary *mutableAttributes = [attributes mutableCopy];
David@0
   803
David@0
   804
		[mutableAttributes removeObjectForKey:NSFontAttributeName];
David@0
   805
David@0
   806
		[mutableAttributes setObject:[font familyName]
David@0
   807
		                      forKey:AIFontFamilyAttributeName];
David@0
   808
		[mutableAttributes setObject:[NSString stringWithFormat:@"%@pt", [NSString stringWithFloat:[font pointSize] maxDigits:2]]
David@0
   809
		                      forKey:AIFontSizeAttributeName];
David@0
   810
David@0
   811
		NSFontTraitMask traits = [[NSFontManager sharedFontManager] traitsOfFont:font];
David@0
   812
		if (traits & NSBoldFontMask) {
David@0
   813
			[mutableAttributes setObject:@"bold" forKey:AIFontWeightAttributeName];
David@0
   814
		}
David@0
   815
		if (traits & NSItalicFontMask) {
David@0
   816
			[mutableAttributes setObject:@"italic" forKey:AIFontStyleAttributeName];
David@0
   817
		}
David@0
   818
David@0
   819
		[font release];
David@0
   820
David@0
   821
		NSDictionary *result = [NSDictionary dictionaryWithDictionary:mutableAttributes];
David@0
   822
		[mutableAttributes release];
David@0
   823
		return result;
David@0
   824
	}
David@0
   825
}
David@0
   826
zacw@1226
   827
- (AIXMLElement *)rootStrictXHTMLElementForAttributedString:(NSAttributedString *)inMessage imagesPath:(NSString *)imagesSavePath
David@0
   828
{
David@0
   829
	NSRange			 searchRange;
David@0
   830
David@0
   831
	//Setup the incoming message as a regular string, and get its length
David@0
   832
	NSString		*inMessageString = [inMessage string];
David@0
   833
	unsigned		 messageLength = [inMessageString length];
David@0
   834
David@0
   835
	NSSet *emptySet = [NSSet set];
David@0
   836
David@0
   837
	//These two stacks are parallel. For every element, there should be a set of attribute names, and vice versa.
David@0
   838
	NSMutableArray *elementStack = [NSMutableArray array];
David@0
   839
	NSMutableArray *attributeNamesStack = [NSMutableArray array];
David@0
   840
David@0
   841
	//Root element: includeHeaders ? <html> : <div>
David@0
   842
David@0
   843
	if (thingsToInclude.headers) {
zacw@1226
   844
		[elementStack addObject:[AIXMLElement elementWithNamespaceName:XMLNamespace elementName:@"html"]];
David@0
   845
		[attributeNamesStack addObject:emptySet];
David@0
   846
zacw@1226
   847
		AIXMLElement *bodyElement = [AIXMLElement elementWithNamespaceName:XMLNamespace elementName:@"body"];
zacw@1226
   848
		[[elementStack lastObject] addObject:bodyElement];
David@0
   849
		[elementStack addObject:bodyElement];
David@0
   850
		[attributeNamesStack addObject:emptySet];
David@0
   851
David@0
   852
		NSColor *pageColor;
David@0
   853
		if ((messageLength > 0) &&
David@0
   854
		   (pageColor = [inMessage attribute:AIBodyColorAttributeName
David@0
   855
									 atIndex:0
David@0
   856
							  effectiveRange:NULL]))
David@0
   857
		{
zacw@1226
   858
			[bodyElement setValue:[@"background-color: " stringByAppendingString:[pageColor CSSRepresentation]] forAttribute:@"style"];
David@0
   859
		}
David@0
   860
	}
David@0
   861
zacw@1226
   862
	AIXMLElement *divElement = [AIXMLElement elementWithNamespaceName:XMLNamespace elementName:@"div"];
David@0
   863
	//If the text is right-to-left, enclose all our HTML in an rtl div tag
David@0
   864
	if ((messageLength > 0) &&
David@0
   865
		([[inMessage attribute:NSParagraphStyleAttributeName
David@0
   866
					   atIndex:0
David@0
   867
				effectiveRange:nil] baseWritingDirection] == NSWritingDirectionRightToLeft))
David@0
   868
	{
zacw@1226
   869
		[divElement setValue:@"rtl" forAttribute:@"dir"];
David@0
   870
	}
zacw@1226
   871
	[[elementStack lastObject] addObject:divElement];
David@0
   872
	[elementStack addObject:divElement];
David@0
   873
	[attributeNamesStack addObject:emptySet];
David@0
   874
David@0
   875
	NSMutableSet *CSSCapableAttributes = [[[NSAttributedString CSSCapableAttributesSet] mutableCopy] autorelease];
David@0
   876
	[CSSCapableAttributes addObject:NSLinkAttributeName];
David@0
   877
	NSSet *CSSCapableAttributesWithNoAttachment = [NSSet setWithSet:CSSCapableAttributes];
David@0
   878
	[CSSCapableAttributes addObject:NSAttachmentAttributeName];
David@0
   879
David@0
   880
	NSDictionary *prevAttributes = nil;
David@0
   881
	//Loop through the entire string, handling each attribute run.
David@0
   882
	searchRange = NSMakeRange(0,messageLength);
David@0
   883
	while (searchRange.location < messageLength) {
David@0
   884
		NSRange runRange;
David@0
   885
		NSDictionary *attributes = [self attributesByReplacingNSFontAttributeNameWithAIFontAttributeNames:[inMessage attributesAtIndex:searchRange.location 
David@0
   886
																												 longestEffectiveRange:&runRange
David@0
   887
																															   inRange:searchRange]];
David@0
   888
		attributes = [attributes dictionaryWithIntersectionWithSetOfKeys:CSSCapableAttributes];
David@0
   889
David@0
   890
		NSSet *startedKeys = nil, *endedKeys = nil;
David@0
   891
		[attributes compareWithPriorDictionary:prevAttributes
David@0
   892
		                          getAddedKeys:&startedKeys
David@0
   893
		                        getRemovedKeys:&endedKeys
David@0
   894
		                    includeChangedKeys:YES];
David@0
   895
		prevAttributes = [attributes dictionaryWithIntersectionWithSetOfKeys:CSSCapableAttributesWithNoAttachment];
David@0
   896
		if([attributes objectForKey:NSAttachmentAttributeName] != nil)
David@0
   897
			runRange.length = 1;  //Encode a single image at a time
David@0
   898
David@0
   899
		NSMutableSet *mutableEndedKeys = [endedKeys mutableCopy];
David@0
   900
		if (mutableEndedKeys) {
David@0
   901
			//First handle attributes that have ended or changed.
David@0
   902
			if ([mutableEndedKeys count]) {
David@0
   903
				NSMutableSet *attributesToRestore = [NSMutableSet set];
David@0
   904
				NSRange popRange = { [attributeNamesStack count], 0 };
David@0
   905
				while ([mutableEndedKeys count]) {
David@0
   906
					--popRange.location; ++popRange.length;
David@0
   907
David@0
   908
					NSMutableSet *attributeNames = [attributeNamesStack objectAtIndex:popRange.location];
David@502
   909
					NSMutableSet *intersection = [[attributeNames mutableCopy] autorelease];
David@502
   910
					[intersection intersectSet:mutableEndedKeys];
David@0
   911
David@0
   912
					[attributeNames minusSet:intersection];
David@0
   913
					[attributesToRestore unionSet:attributeNames];
David@0
   914
David@0
   915
					[mutableEndedKeys minusSet:intersection];
David@0
   916
				}
David@0
   917
				[attributeNamesStack removeObjectsInRange:popRange];
David@0
   918
				[elementStack removeObjectsInRange:popRange];
David@0
   919
David@0
   920
				//Push a new element to restore any attributes that haven't ended.
David@0
   921
				//If no attributes need to be restored, then we do nothing.
David@0
   922
				//If there are attributes to be restored but they're not in the attributes dictionary, then they have in fact ended, and are thus excluded from restoration by the call to -dictionaryWithIntersectionWithSetOfKeys:.
David@0
   923
				if (attributesToRestore && [attributesToRestore count]) {
zacw@1226
   924
					AIXMLElement *restoreElement = [self elementWithAppKitAttributes:[attributes dictionaryWithIntersectionWithSetOfKeys:attributesToRestore]
David@0
   925
					                                                  attributeNames:attributesToRestore
David@0
   926
					                                                  elementContent:nil
David@0
   927
					                             shouldAddElementContentToTopElement:NULL
David@0
   928
																		  imagesPath:imagesSavePath];
zacw@1226
   929
					[[elementStack lastObject] addObject:restoreElement];
David@0
   930
					[elementStack addObject:restoreElement];
David@0
   931
David@0
   932
					[attributeNamesStack addObject:attributesToRestore];
David@0
   933
				}
David@0
   934
			}
David@0
   935
David@0
   936
			[mutableEndedKeys release];
David@0
   937
		}
David@0
   938
David@0
   939
		//Now handle attributes that have started or changed.
David@0
   940
		NSMutableString *elementContent = [[[inMessageString substringWithRange:runRange] mutableCopy] autorelease];
David@0
   941
		
David@0
   942
		BOOL addElementContentToTopElement;
David@0
   943
		if ([startedKeys count]) {
David@0
   944
			//Sort the keys by the length of their range.
David@0
   945
			//First, we build a list of [length, attribute-name] arrays.
David@0
   946
			NSMutableArray *startedKeysArray = [[startedKeys allObjects] mutableCopy];
David@0
   947
			for (unsigned i = 0, count = [startedKeysArray count]; i < count; ++i) {
David@0
   948
				NSRange attributeRange;
David@0
   949
				NSString *attributeName = [startedKeysArray objectAtIndex:i];
David@0
   950
				[inMessage  attribute:attributeName
David@0
   951
				              atIndex:searchRange.location
David@0
   952
				longestEffectiveRange:&attributeRange
David@0
   953
				              inRange:searchRange];
David@0
   954
David@0
   955
				NSMutableArray *item = [[NSMutableArray alloc] initWithCapacity:2];
David@0
   956
				[item addObject:[NSNumber numberWithUnsignedInt:attributeRange.length]];
David@0
   957
				[item addObject:attributeName];
David@0
   958
				[startedKeysArray replaceObjectAtIndex:i withObject:item];
David@0
   959
				[item release];
David@0
   960
			}
David@0
   961
			//Sort. Items will be sorted first by length, then by attribute name.
David@0
   962
			[startedKeysArray sortUsingSelector:@selector(compare:)];
David@0
   963
David@0
   964
			//Consolidate keys by length.
David@0
   965
			for (unsigned i = 0, count = [startedKeysArray count]; i < count; ++i) {
David@0
   966
				NSMutableSet *itemKeys = [NSMutableSet setWithCapacity:1];
David@0
   967
				[itemKeys addObject:[[startedKeysArray objectAtIndex:i] objectAtIndex:1]];
David@0
   968
David@0
   969
				//Eat any equal keys that follow.
David@0
   970
				unsigned j = i + 1;
David@0
   971
				while (
David@0
   972
					(j < count)
David@0
   973
				&&	([[[startedKeysArray objectAtIndex:i] objectAtIndex:0] unsignedIntValue] == [[[startedKeysArray objectAtIndex:j] objectAtIndex:0] unsignedIntValue])
David@0
   974
				) {
David@0
   975
					[itemKeys addObject:[[startedKeysArray objectAtIndex:j] objectAtIndex:1]];
David@0
   976
					[startedKeysArray removeObjectAtIndex:j];
David@0
   977
					--count;
David@0
   978
				}
David@0
   979
David@0
   980
				[[startedKeysArray objectAtIndex:i] replaceObjectAtIndex:1 withObject:itemKeys];
David@0
   981
			}
David@0
   982
David@0
   983
			//Turn each consolidated bunch of keys into an element.
David@0
   984
			addElementContentToTopElement = NO;
David@0
   985
Evan@166
   986
			for (NSArray *item in startedKeysArray) {
David@0
   987
				NSSet *itemKeys = [item objectAtIndex:1];
David@0
   988
David@0
   989
				//Only the last value of addElementContentToTopElement matters here, since we're adding elements to the stack, and that flag relates to the top element.
zacw@1226
   990
				AIXMLElement *thisElement = [self elementWithAppKitAttributes:attributes
David@0
   991
															   attributeNames:itemKeys
David@0
   992
															   elementContent:elementContent
David@0
   993
										  shouldAddElementContentToTopElement:&addElementContentToTopElement
David@0
   994
																   imagesPath:imagesSavePath];
David@0
   995
				if (thisElement) {
zacw@1226
   996
					[[elementStack lastObject] addObject:thisElement];
David@0
   997
					[attributeNamesStack addObject:itemKeys];
David@0
   998
					[elementStack addObject:thisElement];
David@0
   999
				} else if(!addElementContentToTopElement) {
zacw@1226
  1000
					[[elementStack lastObject] addObject:elementContent];
David@0
  1001
				}
David@0
  1002
			}
David@0
  1003
			[startedKeysArray release];
David@0
  1004
David@0
  1005
		} else {
David@0
  1006
			addElementContentToTopElement = YES;
David@0
  1007
		}
David@0
  1008
David@0
  1009
		if (addElementContentToTopElement) {
David@0
  1010
			//Insert an empty BR element between every pair of lines.
zacw@1226
  1011
			AIXMLElement *brElement = [AIXMLElement elementWithNamespaceName:XMLNamespace elementName:@"br"];
zacw@1226
  1012
			[brElement setSelfCloses:YES];
David@0
  1013
			NSArray *linesAndBRs = [elementContent allLinesWithSeparator:brElement];
David@0
  1014
David@0
  1015
			//Add these zero or more lines, with BRs between them, to the top element on the stack.
zacw@1226
  1016
			[[elementStack lastObject] addObjectsFromArray:linesAndBRs];
David@0
  1017
		}
David@0
  1018
David@0
  1019
		searchRange.location += runRange.length;
David@0
  1020
		searchRange.length   -= runRange.length;
David@0
  1021
	}
David@0
  1022
David@0
  1023
	return [elementStack objectAtIndex:0];
David@0
  1024
}
David@0
  1025
David@0
  1026
- (NSString *)encodeStrictXHTML:(NSAttributedString *)inMessage imagesPath:(NSString *)imagesSavePath
David@0
  1027
{
David@0
  1028
	NSString *output = [[self rootStrictXHTMLElementForAttributedString:inMessage imagesPath:imagesSavePath] XMLString];
David@0
  1029
	if (thingsToInclude.headers) {
David@0
  1030
		NSString *doctype = @"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">";
David@0
  1031
		output = [doctype stringByAppendingString:output];
David@0
  1032
	}
zacw@1226
  1033
	
David@0
  1034
	return output;
David@0
  1035
}
David@0
  1036
David@0
  1037
- (NSString *)encodeHTML:(NSAttributedString *)inMessage imagesPath:(NSString *)imagesSavePath
David@0
  1038
{
David@0
  1039
	return thingsToInclude.generateStrictXHTML ? [self encodeStrictXHTML:inMessage imagesPath:imagesSavePath] : [self encodeLooseHTML:inMessage imagesPath:imagesSavePath];
David@0
  1040
}
David@0
  1041
David@0
  1042
- (NSAttributedString *)decodeHTML:(NSString *)inMessage
David@0
  1043
{
David@0
  1044
	return [self decodeHTML:inMessage withDefaultAttributes:nil];
David@0
  1045
}
David@0
  1046
David@0
  1047
- (NSAttributedString *)decodeHTML:(NSString *)inMessage withDefaultAttributes:(NSDictionary *)inDefaultAttributes
David@0
  1048
{
David@0
  1049
	if (!inMessage) return [[[NSAttributedString alloc] init] autorelease];
David@0
  1050
David@0
  1051
	NSScanner					*scanner;
David@0
  1052
	static NSCharacterSet		*tagCharStart = nil, *tagEnd = nil, *charEnd = nil, *absoluteTagEnd = nil;
David@0
  1053
	NSString					*chunkString, *tagOpen;
David@0
  1054
	NSMutableAttributedString	*attrString;
David@0
  1055
	AITextAttributes			*textAttributes;
David@0
  1056
	NSMutableArray				*spanTagChangedAttributesQueue = [NSMutableArray array];
David@0
  1057
	NSMutableArray				*fontTagChangedAttributesQueue = [NSMutableArray array];
David@0
  1058
	NSString					*myBaseURL = [baseURL copy];
David@0
  1059
David@0
  1060
	//Reset the div and span ivars
David@0
  1061
	send = NO;
David@0
  1062
	receive = NO;
David@0
  1063
	inDiv = NO;
David@0
  1064
David@0
  1065
    //set up
David@0
  1066
	if (inDefaultAttributes) {
David@0
  1067
		textAttributes = [AITextAttributes textAttributesWithDictionary:inDefaultAttributes];
David@0
  1068
	} else {
David@0
  1069
		textAttributes = [[[AITextAttributes alloc] init] autorelease];
David@0
  1070
	}
David@0
  1071
David@0
  1072
    attrString = [[NSMutableAttributedString alloc] init];
David@0
  1073
David@0
  1074
	if (!tagCharStart)     tagCharStart = [[NSCharacterSet characterSetWithCharactersInString:@"<&"] retain];
David@0
  1075
	if (!tagEnd)                 tagEnd = [[NSCharacterSet characterSetWithCharactersInString:@" >"] retain];
David@0
  1076
	if (!charEnd)               charEnd = [[NSCharacterSet characterSetWithCharactersInString:@";"] retain];
David@0
  1077
	if (!absoluteTagEnd) absoluteTagEnd = [[NSCharacterSet characterSetWithCharactersInString:@">"] retain];
David@0
  1078
David@0
  1079
	scanner = [NSScanner scannerWithString:inMessage];
David@0
  1080
	[scanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@""]];
David@0
  1081
David@0
  1082
	//Parse the HTML
David@0
  1083
	while (![scanner isAtEnd]) {
David@0
  1084
		/*
David@0
  1085
		 * Scan up to an HTML tag or escaped character.
David@0
  1086
		 *
David@0
  1087
		 * All characters before the next HTML entity are textual characters in the current textAttributes. We append
David@0
  1088
		 * those characters to our final attributed string with the desired attributes before continuing.
David@0
  1089
		 */
David@0
  1090
		NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
David@0
  1091
		if ([scanner scanUpToCharactersFromSet:tagCharStart intoString:&chunkString]) {
David@0
  1092
			id	languageValue = [textAttributes languageValue];
David@0
  1093
			
David@0
  1094
			/* AIM sets language value 143 for characters which are in Symbol, Wingdings, or Webdings.
David@0
  1095
			 * All Symbol characters can be represented in the Symbol font. When the language value is 143, they are being sent
David@0
  1096
			 * as normal ASCII characters and we must run them through stringByConvertingSymbolToSymbolUnicode.
David@0
  1097
			 *
David@0
  1098
			 * Wingdings characters can be represented in the font Wingdings, if available, by offsetting the ASCII characters
David@0
  1099
			 * by 0xF000 to move into the Private  Use space.  If the font is not available, many of them can be represented
David@0
  1100
			 * in any font via our stringByConvertingWingdingsToUnicode conversion table. Wingdings2 and Wingdings3 do not have
David@0
  1101
			 * conversion tables but can also be used as fonts so long as we move the ASCII characters to the Private Use space.
David@0
  1102
			 *
David@0
  1103
			 * Similarly, Webdings can be represented in the font Webdings in the Private Use unicode space.
David@0
  1104
			 */
David@0
  1105
			if (languageValue && ([languageValue intValue] == 143)) {
David@0
  1106
				NSString *fontFamily = [textAttributes fontFamily];
David@0
  1107
David@0
  1108
				if ([fontFamily caseInsensitiveCompare:@"Symbol"] == NSOrderedSame) {
David@0
  1109
					chunkString = [chunkString stringByConvertingSymbolToSymbolUnicode];
David@0
  1110
David@0
  1111
				} else if ([fontFamily rangeOfString:@"Wingdings" options:NSCaseInsensitiveSearch].location != NSNotFound) {
David@0
  1112
					if ([NSFont fontWithName:fontFamily size:0]) {
David@0
  1113
						//Use the font (in is Private Use space) if it is installed
David@0
  1114
						chunkString = [chunkString stringByTranslatingByOffset:0xF000];
David@0
  1115
David@0
  1116
					} else {
David@0
  1117
						chunkString = [chunkString stringByConvertingWingdingsToUnicode];
David@0
  1118
					}
David@0
  1119
David@0
  1120
				} else if ([fontFamily rangeOfString:@"Webdings" options:NSCaseInsensitiveSearch].location != NSNotFound) {
David@0
  1121
					if ([NSFont fontWithName:fontFamily size:0]) {
David@0
  1122
						//Use the Webdings font if it is installed
David@0
  1123
						chunkString = [chunkString stringByTranslatingByOffset:0xF000];
David@0
  1124
					}						
David@0
  1125
				}
David@0
  1126
			}
David@0
  1127
			
David@0
  1128
			[attrString appendString:chunkString withAttributes:[textAttributes dictionary]];
David@0
  1129
		}
David@0
  1130
		
David@0
  1131
		//Process the tag
David@0
  1132
		if ([scanner scanCharactersFromSet:tagCharStart intoString:&tagOpen]) { //If a tag wasn't found, we don't process.
David@0
  1133
			unsigned scanLocation = [scanner scanLocation]; //Remember our location (if this is an invalid tag we'll need to move back)
David@0
  1134
David@0
  1135
			if ([tagOpen isEqualToString:@"<"]) { // HTML <tag>
David@0
  1136
				BOOL		validTag = [scanner scanUpToCharactersFromSet:tagEnd intoString:&chunkString]; //Get the tag
David@0
  1137
				NSString	*charactersToSkipAfterThisTag = nil;
David@0
  1138
David@0
  1139
				if (validTag) { 
David@0
  1140
					//HTML
David@0
  1141
					if ([chunkString caseInsensitiveCompare:@"HTML"] == NSOrderedSame) {
David@0
  1142
						//We ignore most stuff inside the HTML tag, but don't want to see the end of it.
David@0
  1143
						[scanner scanUpToCharactersFromSet:absoluteTagEnd intoString:&chunkString];
David@0
  1144
	
David@0
  1145
					} else if ([chunkString caseInsensitiveCompare:@"/HTML"] == NSOrderedSame) {
David@0
  1146
						//We are done
David@1654
  1147
						[pool release]; pool = nil;
David@0
  1148
						break;
David@0
  1149
David@0
  1150
					//PRE -- ignore attributes for logViewer
David@0
  1151
					} else if ([chunkString caseInsensitiveCompare:@"PRE"] == NSOrderedSame ||
David@0
  1152
							 [chunkString caseInsensitiveCompare:@"/PRE"] == NSOrderedSame) {
David@0
  1153
David@0
  1154
						[scanner scanUpToCharactersFromSet:absoluteTagEnd intoString:&chunkString];
David@0
  1155
David@0
  1156
						//XXX what's going on here?
David@0
  1157
						[textAttributes setTextColor:[NSColor blackColor]];
David@0
  1158
David@0
  1159
					//DIV
David@0
  1160
					} else if ([chunkString caseInsensitiveCompare:@"DIV"] == NSOrderedSame) {
David@0
  1161
						if ([scanner scanUpToCharactersFromSet:absoluteTagEnd
David@0
  1162
													intoString:&chunkString]) {
David@0
  1163
							[self processDivTagArgs:[self parseArguments:chunkString] attributes:textAttributes];
David@0
  1164
						}
David@0
  1165
						inDiv = YES;
David@0
  1166
David@0
  1167
					} else if ([chunkString caseInsensitiveCompare:@"/DIV"] == NSOrderedSame) {
David@0
  1168
						inDiv = NO;
David@0
  1169
David@0
  1170
					//LINK
David@0
  1171
					} else if ([chunkString caseInsensitiveCompare:@"A"] == NSOrderedSame) {
David@0
  1172
						if ([scanner scanUpToCharactersFromSet:absoluteTagEnd intoString:&chunkString]) {
David@0
  1173
							[self processLinkTagArgs:[self parseArguments:chunkString] 
David@0
  1174
										  attributes:textAttributes]; //Process the linktag's contents
David@0
  1175
						}
David@0
  1176
David@0
  1177
					} else if ([chunkString caseInsensitiveCompare:@"/A"] == NSOrderedSame) {
David@0
  1178
						[textAttributes setLinkURL:nil];
David@0
  1179
David@0
  1180
					//Body
David@0
  1181
					} else if ([chunkString caseInsensitiveCompare:@"BODY"] == NSOrderedSame) {
David@0
  1182
						if ([scanner scanUpToCharactersFromSet:absoluteTagEnd intoString:&chunkString]) {
David@0
  1183
							[self processBodyTagArgs:[self parseArguments:chunkString] attributes:textAttributes]; //Process the font tag's contents
David@0
  1184
						}
David@0
  1185
David@0
  1186
					} else if (([chunkString caseInsensitiveCompare:@"/BODY"] == NSOrderedSame) ||
David@0
  1187
							   ([chunkString caseInsensitiveCompare:@"BODY/"] == NSOrderedSame)) {
David@0
  1188
						//ignore
David@0
  1189
David@0
  1190
					//Font
David@0
  1191
					} else if ([chunkString caseInsensitiveCompare:@"FONT"] == NSOrderedSame) {
David@0
  1192
						if ([scanner scanUpToCharactersFromSet:absoluteTagEnd intoString:&chunkString]) {
David@0
  1193
							NSDictionary *changedAttributes;
David@0
  1194
David@0
  1195
							//Process the font tag's contents
David@0
  1196
							changedAttributes = [self processFontTagArgs:[self parseArguments:chunkString] attributes:textAttributes];
David@0
  1197
							[fontTagChangedAttributesQueue addObject:(changedAttributes ? changedAttributes : [NSDictionary dictionary])];
David@0
  1198
						}
David@0
  1199
David@0
  1200
					} else if ([chunkString caseInsensitiveCompare:@"/FONT"] == NSOrderedSame) {
David@0
  1201
						int changedAttributesCount = [fontTagChangedAttributesQueue count];
David@0
  1202
						if (changedAttributesCount) {
David@0
  1203
							[self restoreAttributesFromDict:[fontTagChangedAttributesQueue lastObject] intoAttributes:textAttributes];
David@0
  1204
							[fontTagChangedAttributesQueue removeObjectAtIndex:([fontTagChangedAttributesQueue count] - 1)];	
David@0
  1205
						}
David@0
  1206
						
David@0
  1207
					//span
David@0
  1208
					} else if ([chunkString caseInsensitiveCompare:@"SPAN"] == NSOrderedSame) {
David@0
  1209
						if ([scanner scanUpToCharactersFromSet:absoluteTagEnd intoString:&chunkString]) {
David@0
  1210
							NSDictionary *changedAttributes;
David@0
  1211
David@0
  1212
							changedAttributes = [self processSpanTagArgs:[self parseArguments:chunkString] attributes:textAttributes];
David@0
  1213
							[spanTagChangedAttributesQueue addObject:(changedAttributes ? changedAttributes : [NSDictionary dictionary])];
David@0
  1214
						}
David@0
  1215
David@0
  1216
					} else if ([chunkString caseInsensitiveCompare:@"/SPAN"] == NSOrderedSame) {
David@0
  1217
						int changedAttributesCount = [spanTagChangedAttributesQueue count];
David@0
  1218
						if (changedAttributesCount) {
David@0
  1219
							[self restoreAttributesFromDict:[spanTagChangedAttributesQueue lastObject] intoAttributes:textAttributes];
David@0
  1220
							[spanTagChangedAttributesQueue removeObjectAtIndex:([spanTagChangedAttributesQueue count] - 1)];	
David@0
  1221
						}
David@0
  1222
David@0
  1223
					//Line Break
David@0
  1224
					} else if ([chunkString caseInsensitiveCompare:@"BR"] == NSOrderedSame || 
David@0
  1225
							 [chunkString caseInsensitiveCompare:@"BR/"] == NSOrderedSame ||
David@0
  1226
							 [chunkString caseInsensitiveCompare:@"/BR"] == NSOrderedSame) {
David@0
  1227
						[attrString appendString:@"\n" withAttributes:nil];
David@0
  1228
						
David@0
  1229
						/* Make sure the tag closes; it may have a <BR /> which stopped the scanner at
David@0
  1230
						 * at the space rather than the '>'
David@0
  1231
						 */
David@0
  1232
						[scanner scanUpToCharactersFromSet:absoluteTagEnd intoString:&chunkString];
David@0
  1233
David@0
  1234
						/* Skip any newlines following an HTML line break; if we have one we want to ignore the other.
David@0
  1235
						 * This is generally unnecessary; it is a hack around a winAIM bug where 
David@0
  1236
						 * newlines are sent as "<BR>\n\r"
David@0
  1237
						 */
David@0
  1238
						charactersToSkipAfterThisTag = @"\n\r";
David@0
  1239
David@0
  1240
					//Bold
David@0
  1241
					} else if ([chunkString caseInsensitiveCompare:@"B"] == NSOrderedSame) {
David@0
  1242
						[textAttributes enableTrait:NSBoldFontMask];
David@0
  1243
					} else if ([chunkString caseInsensitiveCompare:@"/B"] == NSOrderedSame) {
David@0
  1244
						[textAttributes disableTrait:NSBoldFontMask];
David@0
  1245
David@0
  1246
					//Strong (interpreted as bold)
David@0
  1247
					} else if ([chunkString caseInsensitiveCompare:@"STRONG"] == NSOrderedSame) {
David@0
  1248
						[textAttributes enableTrait:NSBoldFontMask];
David@0
  1249
					} else if ([chunkString caseInsensitiveCompare:@"/STRONG"] == NSOrderedSame) {
David@0
  1250
						[textAttributes disableTrait:NSBoldFontMask];
David@0
  1251
David@0
  1252
					//Italic
David@0
  1253
					} else if ([chunkString caseInsensitiveCompare:@"I"] == NSOrderedSame) {
David@0
  1254
						[textAttributes enableTrait:NSItalicFontMask];
David@0
  1255
					} else if ([chunkString caseInsensitiveCompare:@"/I"] == NSOrderedSame) {
David@0
  1256
						[textAttributes disableTrait:NSItalicFontMask];
David@0
  1257
David@0
  1258
					//Emphasised (interpreted as italic)
David@0
  1259
					} else if ([chunkString caseInsensitiveCompare:@"EM"] == NSOrderedSame) {
David@0
  1260
						[textAttributes enableTrait:NSItalicFontMask];
David@0
  1261
					} else if ([chunkString caseInsensitiveCompare:@"/EM"] == NSOrderedSame) {
David@0
  1262
						[textAttributes disableTrait:NSItalicFontMask];
David@0
  1263
David@0
  1264
					//Underline
David@0
  1265
					} else if ([chunkString caseInsensitiveCompare:@"U"] == NSOrderedSame) {
David@0
  1266
						[textAttributes setUnderline:YES];
David@0
  1267
					} else if ([chunkString caseInsensitiveCompare:@"/U"] == NSOrderedSame) {
David@0
  1268
						[textAttributes setUnderline:NO];
David@0
  1269
David@0
  1270
					//Strikethrough: <s> is deprecated, but people use it
David@0
  1271
					} else if ([chunkString caseInsensitiveCompare:@"S"] == NSOrderedSame) {
David@0
  1272
						[textAttributes setStrikethrough:YES];
David@0
  1273
					} else if ([chunkString caseInsensitiveCompare:@"/S"] == NSOrderedSame) {
David@0
  1274
						[textAttributes setStrikethrough:NO];
David@0
  1275
David@0
  1276
					// Subscript
David@0
  1277
					} else if ([chunkString caseInsensitiveCompare:@"SUB"] == NSOrderedSame)  {
David@0
  1278
						[textAttributes setSubscript:YES];
David@0
  1279
					} else if ([chunkString caseInsensitiveCompare:@"/SUB"] == NSOrderedSame)  {
David@0
  1280
						[textAttributes setSubscript:NO];
David@0
  1281
David@0
  1282
					// Superscript
David@0
  1283
					} else if ([chunkString caseInsensitiveCompare:@"SUP"] == NSOrderedSame)  {
David@0
  1284
						[textAttributes setSuperscript:YES];
David@0
  1285
					} else if ([chunkString caseInsensitiveCompare:@"/SUP"] == NSOrderedSame)  {
David@0
  1286
						[textAttributes setSuperscript:NO];
David@0
  1287
David@0
  1288
					//Image
David@0
  1289
					} else if ([chunkString caseInsensitiveCompare:@"IMG"] == NSOrderedSame) {
David@0
  1290
						if ([scanner scanUpToCharactersFromSet:absoluteTagEnd intoString:&chunkString]) {
David@0
  1291
							NSAttributedString *attachString = [self processImgTagArgs:[self parseArguments:chunkString] 
David@0
  1292
																			attributes:textAttributes
David@0
  1293
																			   baseURL:myBaseURL];
David@0
  1294
							if (attachString) {
David@0
  1295
								[attrString appendAttributedString:attachString];
David@0
  1296
							}
David@0
  1297
						}
David@0
  1298
					} else if ([chunkString caseInsensitiveCompare:@"/IMG"] == NSOrderedSame) {
David@0
  1299
						//just ignore </img> if we find it
David@0
  1300
David@0
  1301
					//Horizontal Rule
David@0
  1302
					} else if ([chunkString caseInsensitiveCompare:@"HR"] == NSOrderedSame) {
David@0
  1303
						[attrString appendString:horizontalRule withAttributes:nil];
David@0
  1304
						
David@0
  1305
					// Ignore <p> for those wacky AIM express users
David@0
  1306
					} else if ([chunkString caseInsensitiveCompare:@"P"] == NSOrderedSame ||
David@0
  1307
							   ([chunkString caseInsensitiveCompare:@"/P"] == NSOrderedSame)) {
David@0
  1308
						
David@0
  1309
					// Ignore <head> tags
David@0
  1310
					} else if ([chunkString caseInsensitiveCompare:@"HEAD"] == NSOrderedSame ||
David@0
  1311
							   ([chunkString caseInsensitiveCompare:@"/HEAD"] == NSOrderedSame)) {
David@0
  1312
						[scanner scanUpToCharactersFromSet:absoluteTagEnd intoString:&chunkString];
David@0
  1313
						
David@0
  1314
					//Base URL tag
David@0
  1315
					} else if ([chunkString caseInsensitiveCompare:@"BASE"] == NSOrderedSame) {
David@0
  1316
						if ([scanner scanUpToCharactersFromSet:absoluteTagEnd intoString:&chunkString]) {
David@0
  1317
							[myBaseURL release];
David@0
  1318
							myBaseURL = [[[self parseArguments:chunkString] objectForKey:@"href"] retain];
David@0
  1319
						}
Evan@366
  1320
					//Ignore <meta> tags
David@0
  1321
					} else if ([chunkString caseInsensitiveCompare:@"META"] == NSOrderedSame ||
David@0
  1322
							   ([chunkString caseInsensitiveCompare:@"/META"] == NSOrderedSame)) {
David@0
  1323
						[scanner scanUpToCharactersFromSet:absoluteTagEnd intoString:&chunkString];
Evan@366
  1324
					
Evan@366
  1325
					//Ignore <ul>, </ul>, and </li>
Evan@366
  1326
					} else if (([chunkString caseInsensitiveCompare:@"UL"] == NSOrderedSame) ||
Evan@366
  1327
							   ([chunkString caseInsensitiveCompare:@"/UL"] == NSOrderedSame) ||
Evan@366
  1328
							   ([chunkString caseInsensitiveCompare:@"/LI"] == NSOrderedSame)) {
Evan@366
  1329
Evan@366
  1330
					//Convert <li> into a bullet point
Evan@366
  1331
					} else if ([chunkString caseInsensitiveCompare:@"LI"] == NSOrderedSame) {
Evan@367
  1332
						[attrString appendString:@"• " withAttributes:[textAttributes dictionary]];
Evan@366
  1333
	
David@0
  1334
					//Invalid
David@0
  1335
					} else {
David@0
  1336
						validTag = NO;
David@0
  1337
					}
David@0
  1338
				}
David@0
  1339
David@0
  1340
				//Skip over the end tag character '>' and any other characters we want to skip
David@0
  1341
				if (validTag) {
David@0
  1342
					//Get to the > if we're not there already, as will happen with XML namespacing...
David@0
  1343
					[scanner scanUpToCharactersFromSet:absoluteTagEnd intoString:NULL];
David@0
  1344
David@0
  1345
					//And skip it
David@0
  1346
					if (![scanner isAtEnd]) {
David@0
  1347
						[scanner setScanLocation:[scanner scanLocation]+1];
David@0
  1348
						
David@0
  1349
						//Skip any other characters we are supposed to skip before continuing
David@0
  1350
						if (charactersToSkipAfterThisTag) {
David@0
  1351
							NSCharacterSet *charSetToSkip;
David@0
  1352
							
David@0
  1353
							charSetToSkip = [NSCharacterSet characterSetWithCharactersInString:charactersToSkipAfterThisTag];
David@0
  1354
							[scanner scanCharactersFromSet:charSetToSkip
David@0
  1355
												intoString:NULL];
David@0
  1356
						}
David@0
  1357
					}
David@0
  1358
					
David@0
  1359
				} else {
David@0
  1360
					//When an invalid tag is encountered, we add the <, and then move our scanner back to continue processing
David@0
  1361
					[attrString appendString:@"<" withAttributes:[textAttributes dictionary]];
David@0
  1362
					[scanner setScanLocation:scanLocation];
David@0
  1363
				}
David@0
  1364
David@0
  1365
			} else if ([tagOpen compare:@"&"] == NSOrderedSame) { // escape character, eg &gt;
David@0
  1366
				BOOL validTag = [scanner scanUpToCharactersFromSet:charEnd intoString:&chunkString];
David@0
  1367
David@0
  1368
				if (validTag) {
David@0
  1369
					// We could upgrade this to use an NSDictionary with lots of chars
David@0
  1370
					// but for now, if-blocks will do
David@0
  1371
					if ([chunkString caseInsensitiveCompare:@"GT"] == NSOrderedSame) {
David@0
  1372
						[attrString appendString:@">" withAttributes:[textAttributes dictionary]];
David@0
  1373
David@0
  1374
					} else if ([chunkString caseInsensitiveCompare:@"LT"] == NSOrderedSame) {
David@0
  1375
						[attrString appendString:@"<" withAttributes:[textAttributes dictionary]];
David@0
  1376
David@0
  1377
					} else if ([chunkString caseInsensitiveCompare:@"AMP"] == NSOrderedSame) {
David@0
  1378
						[attrString appendString:@"&" withAttributes:[textAttributes dictionary]];
David@0
  1379
David@0
  1380
					} else if ([chunkString caseInsensitiveCompare:@"QUOT"] == NSOrderedSame) {
David@0
  1381
						[attrString appendString:@"\"" withAttributes:[textAttributes dictionary]];
David@0
  1382
David@0
  1383
					} else if ([chunkString caseInsensitiveCompare:@"APOS"] == NSOrderedSame) {
David@0
  1384
						[attrString appendString:@"'" withAttributes:[textAttributes dictionary]];
David@0
  1385
David@0
  1386
					} else if ([chunkString caseInsensitiveCompare:@"NBSP"] == NSOrderedSame) {
David@0
  1387
						[attrString appendString:@" " withAttributes:[textAttributes dictionary]];
David@0
  1388
David@0
  1389
					} else if ([chunkString hasPrefix:@"#x"]) {
David@0
  1390
						NSString *hexString = [chunkString substringFromIndex:2];
David@0
  1391
						NSScanner *hexScanner = [NSScanner scannerWithString:hexString];
David@0
  1392
						unsigned int character = 0;
David@0
  1393
						if([hexScanner scanHexInt:&character])
David@0
  1394
							[attrString appendString:[NSString stringWithFormat:@"%C", character]
David@0
  1395
									  withAttributes:[textAttributes dictionary]];
David@0
  1396
					} else if ([chunkString hasPrefix:@"#"]) {
David@0
  1397
						NSString *decString = [chunkString substringFromIndex:1];
David@0
  1398
						NSScanner *decScanner = [NSScanner scannerWithString:decString];
David@0
  1399
						int character = 0;
David@0
  1400
						if([decScanner scanInt:&character])
David@0
  1401
							[attrString appendString:[NSString stringWithFormat:@"%C", character]
David@0
  1402
									  withAttributes:[textAttributes dictionary]];
David@0
  1403
					}
David@0
  1404
					else { //Invalid
David@0
  1405
						validTag = NO;
David@0
  1406
					}
David@0
  1407
				}
David@0
  1408
David@0
  1409
				if (validTag) { //Skip over the end tag character ';'.  Don't scan all of that character, however, as we'll skip ;; and so on.
David@0
  1410
					if (![scanner isAtEnd])
David@0
  1411
						[scanner setScanLocation:[scanner scanLocation] + 1];
David@0
  1412
				} else {
David@0
  1413
					//When an invalid tag is encountered, we add the &, and then move our scanner back to continue processing
David@0
  1414
					[attrString appendString:@"&" withAttributes:[textAttributes dictionary]];
David@0
  1415
					[scanner setScanLocation:scanLocation];
David@0
  1416
				}
David@0
  1417
			} else { //Invalid tag character (most likely a stray < or &)
David@0
  1418
				if ([tagOpen length] > 1) {
David@0
  1419
					//If more than one character was scanned, add the first one, and move the scanner back to re-process the additional characters
David@0
  1420
					[attrString appendString:[tagOpen substringToIndex:1] withAttributes:[textAttributes dictionary]];
David@0
  1421
					[scanner setScanLocation:[scanner scanLocation] - ([tagOpen length]-1)]; 
David@0
  1422
				} else {
David@0
  1423
					[attrString appendString:tagOpen withAttributes:[textAttributes dictionary]];
David@0
  1424
				}
David@0
  1425
			}
David@0
  1426
		}
David@0
  1427
		[pool release];
David@0
  1428
	}
David@0
  1429
	
David@0
  1430
	/* If the string has a constant NSBackgroundColorAttributeName attribute and no AIBodyColorAttributeName,
David@0
  1431
	 * we want to move the NSBackgroundColorAttributeName attribute to AIBodyColorAttributeName (Things are a
David@0
  1432
	 * lot more attractive this way).
David@0
  1433
	 */
David@0
  1434
	if ([attrString length]) {
David@0
  1435
		NSRange backRange;
David@0
  1436
		NSColor *bodyColor = [attrString attribute:NSBackgroundColorAttributeName 
David@0
  1437
										   atIndex:0 
David@0
  1438
									effectiveRange:&backRange];
David@0
  1439
		if (bodyColor && (backRange.length == [attrString length])) {
David@0
  1440
			[attrString addAttribute:AIBodyColorAttributeName
David@0
  1441
							   value:bodyColor 
David@0
  1442
							   range:NSMakeRange(0,[attrString length])];
David@0
  1443
			[attrString removeAttribute:NSBackgroundColorAttributeName 
David@0
  1444
								  range:NSMakeRange(0,[attrString length])];
David@0
  1445
		}
David@0
  1446
	}
David@0
  1447
David@0
  1448
	[myBaseURL release];
David@0
  1449
David@0
  1450
	return [attrString autorelease];
David@0
  1451
}
David@0
  1452
David@0
  1453
#pragma mark Tag-parsing
David@0
  1454
David@0
  1455
/*methods in this section take a parsed tag (see -parseArguments:) and transfer
David@0
  1456
 *  its specification to a text-attributes object.
David@0
  1457
 */
David@0
  1458
David@0
  1459
- (void)restoreAttributesFromDict:(NSDictionary *)inAttributes intoAttributes:(AITextAttributes *)textAttributes
Evan@166
  1460
{	
Evan@166
  1461
	for (NSString *key in inAttributes) {
David@0
  1462
		id value = [inAttributes objectForKey:key];
David@0
  1463
		SEL selector = NSSelectorFromString(key);
David@0
  1464
		if (value == [NSNull null]) value = nil;
David@0
  1465
		
David@0
  1466
		[textAttributes performSelector:selector
David@0
  1467
							 withObject:value];
David@0
  1468
	}
David@0
  1469
}
David@0
  1470
David@0
  1471
//Process the contents of a font tag
David@0
  1472
- (NSDictionary *)processFontTagArgs:(NSDictionary *)inArgs attributes:(AITextAttributes *)textAttributes
David@0
  1473
{
David@0
  1474
	NSMutableDictionary	*originalAttributes = [NSMutableDictionary dictionary];
David@0
  1475
Evan@166
  1476
	for (NSString *arg in inArgs) {
David@0
  1477
		if ([arg caseInsensitiveCompare:@"face"] == NSOrderedSame) {
David@0
  1478
			[originalAttributes setObject:([textAttributes fontFamily] ? (id)[textAttributes fontFamily] : (id)[NSNull null])
David@0
  1479
								   forKey:@"setFontFamily:"];
David@0
  1480
David@0
  1481
			[textAttributes setFontFamily:[inArgs objectForKey:arg]];
David@0
  1482
David@0
  1483
		} else if ([arg caseInsensitiveCompare:@"size"] == NSOrderedSame) {
David@0
  1484
			//Always prefer an ABSZ to a size
David@0
  1485
			if (![inArgs objectForKey:@"ABSZ"] && ![inArgs objectForKey:@"absz"]) {
David@0
  1486
				unsigned absSize = [[inArgs objectForKey:arg] intValue];
David@0
  1487
				static int pointSizes[] = { 9, 10, 12, 14, 18, 24, 48, 72 };
David@0
  1488
				int size = (absSize <= 8 ? pointSizes[absSize-1] : 12);
David@0
  1489
				
David@0
  1490
				[originalAttributes setObject:[NSNumber numberWithInt:[textAttributes fontSize]]
David@0
  1491
									   forKey:@"setFontSizeFromNumber:"];
David@0
  1492
David@0
  1493
				[textAttributes setFontSize:size];
David@0
  1494
			}
David@0
  1495
David@0
  1496
		} else if ([arg caseInsensitiveCompare:@"absz"] == NSOrderedSame) {
David@0
  1497
			[originalAttributes setObject:[NSNumber numberWithInt:[textAttributes fontSize]]
David@0
  1498
								   forKey:@"setFontSizeFromNumber:"];
David@0
  1499
David@0
  1500
			[textAttributes setFontSize:[[inArgs objectForKey:arg] intValue]];
David@0
  1501
David@0
  1502
		} else if ([arg caseInsensitiveCompare:@"color"] == NSOrderedSame) {
David@0
  1503
			[originalAttributes setObject:([textAttributes textColor] ? (id)[textAttributes textColor] : (id)[NSNull null])
David@0
  1504
								   forKey:@"setTextColor:"];
David@0
  1505
David@0
  1506
			[textAttributes setTextColor:[NSColor colorWithHTMLString:[inArgs objectForKey:arg] 
David@0
  1507
														 defaultColor:[NSColor blackColor]]];
David@0
  1508
David@0
  1509
		} else if ([arg caseInsensitiveCompare:@"back"] == NSOrderedSame) {
David@0
  1510
			[originalAttributes setObject:([textAttributes textBackgroundColor] ? (id)[textAttributes textBackgroundColor] : (id)[NSNull null])
David@0
  1511
								   forKey:@"setTextBackgroundColor:"];
David@0
  1512
David@0
  1513
			[textAttributes setTextBackgroundColor:[NSColor colorWithHTMLString:[inArgs objectForKey:arg]
David@0
  1514
																   defaultColor:[NSColor whiteColor]]];
David@0
  1515
David@0
  1516
		} else if ([arg caseInsensitiveCompare:@"lang"] == NSOrderedSame) {
David@0
  1517
			[originalAttributes setObject:([textAttributes languageValue] ? (id)[textAttributes languageValue] : (id)[NSNull null])
David@0
  1518
								   forKey:@"setLanguageValue:"];
David@0
  1519
David@0
  1520
			[textAttributes setLanguageValue:[inArgs objectForKey:arg]];
David@0
  1521
David@0
  1522
		}  else if ([arg caseInsensitiveCompare:@"sender"] == NSOrderedSame) {
David@0
  1523
			//Ghetto HTML log processing
David@0
  1524
			if (inDiv && send) {
David@0
  1525
				[originalAttributes setObject:([textAttributes textColor] ? (id)[textAttributes textColor] : (id)[NSNull null])
David@0
  1526
					
David@0
  1527
									   forKey:@"setTextColor:"];
David@0
  1528
				[textAttributes setTextColor:[NSColor colorWithCalibratedRed:0.0 green:0.5 blue:0.0 alpha:1.0]];
David@0
  1529
David@0
  1530
			} else if (inDiv && receive) {
David@0
  1531
				[originalAttributes setObject:([textAttributes textColor] ? (id)[textAttributes textColor] : (id)[NSNull null])
David@0
  1532
									   forKey:@"setTextColor:"];
David@0
  1533
				
David@0
  1534
				[textAttributes setTextColor:[NSColor colorWithCalibratedRed:0.0 green:0.0 blue:0.5 alpha:1.0]];
David@0
  1535
			}
David@0
  1536
		}	
David@0
  1537
	}
David@0
  1538
David@0
  1539
	return originalAttributes;
David@0
  1540
}
David@0
  1541
David@0
  1542
- (void)processBodyTagArgs:(NSDictionary *)inArgs attributes:(AITextAttributes *)textAttributes
David@0
  1543
{
Evan@166
  1544
	for (NSString *arg in inArgs) {
David@0
  1545
		if ([arg caseInsensitiveCompare:@"bgcolor"] == NSOrderedSame) {
David@0
  1546
			[textAttributes setBackgroundColor:[NSColor colorWithHTMLString:[inArgs objectForKey:arg] defaultColor:[NSColor whiteColor]]];
David@0
  1547
		}	
David@0
  1548
	}
David@0
  1549
}
David@0
  1550
David@0
  1551
- (NSDictionary *)processSpanTagArgs:(NSDictionary *)inArgs attributes:(AITextAttributes *)textAttributes
David@0
  1552
{
David@0
  1553
	NSMutableDictionary	*originalAttributes = [NSMutableDictionary dictionary];
David@0
  1554
Evan@166
  1555
	for (NSString *arg in inArgs) {
David@0
  1556
		if ([arg caseInsensitiveCompare:@"class"] == NSOrderedSame) {
David@0
  1557
			//Process the span tag if it's in a log
David@0
  1558
			NSString	*class = [inArgs objectForKey:arg];
David@0
  1559
David@0
  1560
			if ([class caseInsensitiveCompare:@"sender"] == NSOrderedSame) {
David@0
  1561
				if (inDiv && send) {
David@0
  1562
					[originalAttributes setObject:([textAttributes textColor] ? (id)[textAttributes textColor] : (id)[NSNull null])
David@0
  1563
										   forKey:@"setTextColor:"];
David@0
  1564
					[textAttributes setTextColor:[NSColor colorWithCalibratedRed:0.0 
David@0
  1565
																		   green:0.5
David@0
  1566
																			blue:0.0 
David@0
  1567
																		   alpha:1.0]];
David@0
  1568
				} else if (inDiv && receive) {
David@0
  1569
					[originalAttributes setObject:([textAttributes textColor] ? (id)[textAttributes textColor] : (id)[NSNull null])
David@0
  1570
										   forKey:@"setTextColor:"];
David@0
  1571
					[textAttributes setTextColor:[NSColor colorWithCalibratedRed:0.0
David@0
  1572
																		   green:0.0
David@0
  1573
																			blue:0.5 
David@0
  1574
																		   alpha:1.0]];
David@0
  1575
				}
David@0
  1576
David@0
  1577
			} else if ([class caseInsensitiveCompare:@"timestamp"] == NSOrderedSame) {
David@0
  1578
				[originalAttributes setObject:([textAttributes textColor] ? (id)[textAttributes textColor] : (id)[NSNull null])
David@0
  1579
									   forKey:@"setTextColor:"];
David@0
  1580
				[textAttributes setTextColor:[NSColor grayColor]];
David@0
  1581
			}
David@0
  1582
		} else if ([arg caseInsensitiveCompare:@"style"] == NSOrderedSame) {
zacw@1502
  1583
			NSString	*styleList = [inArgs objectForKey:arg];
David@0
  1584
			NSRange		attributeRange;
zacw@1502
  1585
			
zacw@1502
  1586
			NSScanner	*styleScanner = [NSScanner scannerWithString:styleList];
zacw@1502
  1587
			NSString	*style;
David@0
  1588
zacw@1502
  1589
			while([styleScanner scanUpToString:@";" intoString:&style])
zacw@1502
  1590
			{
zacw@1502
  1591
				[styleScanner scanString:@";" intoString:nil];
zacw@1502
  1592
				
zacw@1502
  1593
				int styleLength = [style length];
zacw@1502
  1594
zacw@1502
  1595
				attributeRange = [style rangeOfString:@"font-family:" options:NSCaseInsensitiveSearch];
zacw@1502
  1596
				if (attributeRange.location != NSNotFound) {
zacw@1502
  1597
					NSString *fontFamily = [[style substringWithRange:NSMakeRange(NSMaxRange(attributeRange), styleLength - NSMaxRange(attributeRange))]
zacw@1502
  1598
												 stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
zacw@1502
  1599
David@0
  1600
					[originalAttributes setObject:([textAttributes fontFamily] ? (id)[textAttributes fontFamily] : (id)[NSNull null])
David@0
  1601
										   forKey:@"setFontFamily:"];
zacw@1502
  1602
					
David@0
  1603
					[textAttributes setFontFamily:fontFamily];
David@0
  1604
				}
zacw@1502
  1605
				
zacw@1502
  1606
				attributeRange = [style rangeOfString:@"font-size:" options:NSCaseInsensitiveSearch];
zacw@1502
  1607
				if (attributeRange.location != NSNotFound) {
zacw@1502
  1608
					NSString *fontSize = [[style substringWithRange:NSMakeRange(NSMaxRange(attributeRange), styleLength-NSMaxRange(attributeRange))]
zacw@1502
  1609
											 stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
David@0
  1610
					
David@0
  1611
					static int stylePointSizes[] = { 9, 10, 12, 14, 18, 24 };
David@0
  1612
					int size = 12;
David@0
  1613
					
David@0
  1614
					if ([fontSize caseInsensitiveCompare:@"xx-small"] == NSOrderedSame) {
David@0
  1615
						size = stylePointSizes[0];
David@0
  1616
						
David@0
  1617
					} else if ([fontSize caseInsensitiveCompare:@"x-small"] == NSOrderedSame) {
David@0
  1618
						size = stylePointSizes[1];
David@0
  1619
						
David@0
  1620
					} else if ([fontSize caseInsensitiveCompare:@"small"] == NSOrderedSame) {
David@0
  1621
						size = stylePointSizes[2];
David@0
  1622
						
David@0
  1623
					} else if ([fontSize caseInsensitiveCompare:@"medium"] == NSOrderedSame) {
David@0
  1624
						size = stylePointSizes[3];
David@0
  1625
						
David@0
  1626
					} else if ([fontSize caseInsensitiveCompare:@"large"] == NSOrderedSame) {
David@0
  1627
						size = stylePointSizes[4];
David@0
  1628
						
David@0
  1629
					} else if ([fontSize caseInsensitiveCompare:@"x-large"] == NSOrderedSame) {
David@0
  1630
						size = stylePointSizes[5];
zacw@1502
  1631
					} else {
zacw@1502
  1632
						NSRange	 pixelUnits = [fontSize	rangeOfString:@"px"
zacw@1502
  1633
														options:NSLiteralSearch];
zacw@1502
  1634
						if (pixelUnits.location != NSNotFound) {
zacw@1502
  1635
							size = [[fontSize substringWithRange:NSMakeRange(0,pixelUnits.location)] intValue];
zacw@1502
  1636
						}
David@0
  1637
					}
David@0
  1638
					
David@0
  1639
					[originalAttributes setObject:[NSNumber numberWithInt:[textAttributes fontSize]]
David@0
  1640
										   forKey:@"setFontSizeFromNumber:"];
David@0
  1641
					[textAttributes setFontSize:size];
David@0
  1642
				}
David@0
  1643
zacw@1502
  1644
				attributeRange = [style rangeOfString:@"font-weight:" options:NSCaseInsensitiveSearch];
zacw@1502
  1645
				if (attributeRange.location != NSNotFound) {
zacw@1502
  1646
					NSString *fontWeight = [[style substringWithRange:NSMakeRange(NSMaxRange(attributeRange), styleLength - NSMaxRange(attributeRange))]
zacw@1502
  1647
												stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
zacw@1502
  1648
David@0
  1649
					[originalAttributes setObject:[NSNumber numberWithUnsignedInt:[textAttributes traits]]
David@0
  1650
										   forKey:@"setTraits:"];
David@0
  1651
					if (([fontWeight caseInsensitiveCompare:@"bold"] == NSOrderedSame) ||
David@0
  1652
						([fontWeight caseInsensitiveCompare:@"bolder"] == NSOrderedSame) ||
David@0
  1653
						([fontWeight intValue] > 400)) {
David@0
  1654
						[textAttributes enableTrait:NSBoldFontMask];
David@0
  1655
					} else {
David@0
  1656
						[textAttributes disableTrait:NSBoldFontMask];						
David@0
  1657
					}
David@0
  1658
				}
zacw@1502
  1659
				
zacw@1502
  1660
				attributeRange = [style rangeOfString:@"font-style:" options:NSCaseInsensitiveSearch];
zacw@1502
  1661
				if (attributeRange.location != NSNotFound) {
zacw@1502
  1662
					NSString *fontStyle = [[style substringWithRange:NSMakeRange(NSMaxRange(attributeRange), styleLength - NSMaxRange(attributeRange))]
zacw@1502
  1663
											 stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
zacw@1502
  1664
David@0
  1665
					[originalAttributes setObject:[NSNumber numberWithUnsignedInt:[textAttributes traits]]
David@0
  1666
																 forKey:@"setTraits:"];
David@0
  1667
					if (([fontStyle caseInsensitiveCompare:@"italic"] == NSOrderedSame) ||
David@0
  1668
							([fontStyle caseInsensitiveCompare:@"oblique"] == NSOrderedSame)) {
David@0
  1669
						[textAttributes enableTrait:NSItalicFontMask];
David@0
  1670
					} else {
David@0
  1671
						[textAttributes disableTrait:NSItalicFontMask];
David@0
  1672
					}
David@0
  1673
				}
David@0
  1674
zacw@1502
  1675
				attributeRange = [style rangeOfString:@"font:" options:NSCaseInsensitiveSearch];
zacw@1502
  1676
				if (attributeRange.location != NSNotFound) {
zacw@1502
  1677
					NSString *fontString = [[style substringWithRange:NSMakeRange(NSMaxRange(attributeRange), styleLength - NSMaxRange(attributeRange))]
zacw@1502
  1678
												 stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
zacw@1502
  1679
zacw@1502
  1680
					NSScanner *fontStringScanner = [NSScanner scannerWithString:fontString];
zacw@1502
  1681
					float fontSize;
zacw@1502
  1682
					if([fontStringScanner scanFloat:&fontSize])
zacw@1502
  1683
						[textAttributes setFontSize:fontSize];
zacw@1502
  1684
					[fontStringScanner scanUpToString:@" " intoString:nil];
zacw@1502
  1685
					[fontStringScanner setScanLocation:[fontStringScanner scanLocation] + 1];
zacw@1502
  1686
					NSString *font = [fontString substringFromIndex:[fontStringScanner scanLocation]];
zacw@1502
  1687
					if ([font length]) {
David@0
  1688
						[originalAttributes setObject:([textAttributes fontFamily] ? (id)[textAttributes fontFamily] : (id)[NSNull null])
David@0
  1689
											   forKey:@"setFontFamily:"];
zacw@1502
  1690
						[textAttributes setFontFamily:font];
David@0
  1691
					}
David@0
  1692
				}
David@0
  1693
zacw@1502
  1694
				attributeRange = [style rangeOfString:@"background-color:" options:NSCaseInsensitiveSearch];
zacw@1502
  1695
				if (attributeRange.location != NSNotFound) {
zacw@1502
  1696
					NSString *hexColor = [[style substringWithRange:NSMakeRange(NSMaxRange(attributeRange), styleLength - NSMaxRange(attributeRange))]
zacw@1502
  1697
											 stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
zacw@1502
  1698
David@0
  1699
					[originalAttributes setObject:([textAttributes backgroundColor] ? (id)[textAttributes backgroundColor] : (id)[NSNull null])
David@0
  1700
										   forKey:@"setBackgroundColor:"];
David@0
  1701
					[textAttributes setBackgroundColor:[NSColor colorWithHTMLString:hexColor
David@0
  1702
																	   defaultColor:[NSColor blackColor]]];
zacw@1502
  1703
zacw@1502
  1704
					//Take out the background-color attribute, so that the following search for color: does not match it.
zacw@1502
  1705
					NSMutableString *mStyle = [[style mutableCopy] autorelease];
zacw@1502
  1706
					[mStyle replaceCharactersInRange:attributeRange
zacw@1502
  1707
										  withString:@"onpxtebhaq-pbybe:"]; //ROT13('background-color: ')
zacw@1502
  1708
					style = mStyle;
David@0
  1709
				}
David@0
  1710
zacw@1502
  1711
				attributeRange = [style rangeOfString:@"color:" options:NSCaseInsensitiveSearch];
zacw@1502
  1712
				if (attributeRange.location != NSNotFound) {
zacw@1502
  1713
					NSString *hexColor = [[style substringWithRange:NSMakeRange(NSMaxRange(attributeRange), styleLength - NSMaxRange(attributeRange))]
zacw@1502
  1714
											 stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
David@0
  1715
David@0
  1716
					[originalAttributes setObject:([textAttributes textColor] ? (id)[textAttributes textColor] : (id)[NSNull null])
David@0
  1717
										   forKey:@"setTextColor:"];
David@0
  1718
					[textAttributes setTextColor:[NSColor colorWithHTMLString:hexColor
David@0
  1719
																 defaultColor:[NSColor blackColor]]];
David@0
  1720
				}
zacw@1502
  1721
				
zacw@1502
  1722
				attributeRange = [style rangeOfString:@"text-decoration:" options:NSCaseInsensitiveSearch];
zacw@1502
  1723
				if (attributeRange.location != NSNotFound) {
zacw@1502
  1724
					NSString *decoration = [[style substringWithRange:NSMakeRange(NSMaxRange(attributeRange), styleLength - NSMaxRange(attributeRange))]
zacw@1502
  1725
												 stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
zacw@1502
  1726
David@0
  1727
					if ([decoration caseInsensitiveCompare:@"none"] == NSOrderedSame){
David@0
  1728
						[originalAttributes setObject:([textAttributes underline] ? [NSNumber numberWithBool:[textAttributes underline]] : (id)[NSNull null]) forKey:@"setUnderline:"];
David@0
  1729
						[textAttributes setUnderline: NO];
David@0
  1730
						[originalAttributes setObject:([textAttributes strikethrough] ? [NSNumber numberWithBool:[textAttributes strikethrough]] : (id)[NSNull null]) forKey:@"setStrikethrough:"];
David@0
  1731
						[textAttributes setStrikethrough: NO];
David@0
  1732
						
David@0
  1733
					} else if ([decoration caseInsensitiveCompare:@"underline"] == NSOrderedSame){
David@0
  1734
						[originalAttributes setObject:([textAttributes underline] ? [NSNumber numberWithBool:[textAttributes underline]] : (id)[NSNull null]) forKey:@"setUnderline:"];
David@0
  1735
						[textAttributes setUnderline: YES];
David@0
  1736
					} else if ([decoration caseInsensitiveCompare:@"overline"] == NSOrderedSame){
David@0
  1737
					
David@0
  1738
					} else if ([decoration caseInsensitiveCompare:@"line-through"] == NSOrderedSame){
David@0
  1739
						[originalAttributes setObject:([textAttributes strikethrough] ? [NSNumber numberWithBool:[textAttributes strikethrough]] : (id)[NSNull null]) forKey:@"setStrikethrough:"];
David@0
  1740
						[textAttributes setStrikethrough: YES];
David@0
  1741
					} else if ([decoration caseInsensitiveCompare:@"blink"] == NSOrderedSame){
David@0
  1742
						
David@0
  1743
					}
David@0
  1744
				}
David@0
  1745
			}
David@0
  1746
        }
David@0
  1747
	}
David@0
  1748
	
David@0
  1749
	return originalAttributes;
David@0
  1750
}
David@0
  1751
David@0
  1752
- (void)processLinkTagArgs:(NSDictionary *)inArgs attributes:(AITextAttributes *)textAttributes
David@0
  1753
{
Evan@166
  1754
	for (NSString *arg in inArgs) {
David@0
  1755
		if ([arg caseInsensitiveCompare:@"href"] == NSOrderedSame) {
David@0
  1756
			NSString	*linkString = [inArgs objectForKey:arg];
David@0
  1757
			
David@0
  1758
			/* Replace any AIM-specific %n occurances with their escaped version.
David@0
  1759
			 * Note: It seems like this would be a good place to use CFURLCreateStringByReplacingPercentEscapes()
David@0
  1760
			 * and then CFURLCreateStringByAddingPercentEscapes().  Unfortunately, CFURLCreateStringByReplacingPercentEscapes()
David@0
  1761
			 * returns NULL if any percent escapes are invalid... and %n is decidedly invalid.
David@0
  1762
			 */
David@0
  1763
			if ([linkString rangeOfString:@"%n"].location != NSNotFound) {
David@0
  1764
				NSMutableString	*newLinkString = [[linkString mutableCopy] autorelease];
David@0
  1765
				[newLinkString replaceOccurrencesOfString:@"%n"
David@0
  1766
											   withString:@"%25n"
David@0
  1767
												  options:NSLiteralSearch
David@0
  1768
													range:NSMakeRange(0, [newLinkString length])];
David@0
  1769
				linkString = newLinkString;
David@0
  1770
			}
David@0
  1771
			
David@0
  1772
			//NSURL does not expect an HTML-escaped string, but HTML-escape codes are valid within links (e.g. &amp;)
David@0
  1773
			linkString = [linkString stringByUnescapingFromXMLWithEntities:nil];
David@0
  1774
Evan@362
  1775
			if (baseURL)
Evan@366
  1776
				[textAttributes setLinkURL:[NSURL URLWithString:linkString relativeToURL:[NSURL URLWithString:baseURL]]];
Evan@362
  1777
			else
Evan@362
  1778
				[textAttributes setLinkURL:[NSURL URLWithString:linkString]];
David@0
  1779
		}
David@0
  1780
	}
David@0
  1781
}
David@0
  1782
David@0
  1783
- (void)processDivTagArgs:(NSDictionary *)inArgs attributes:(AITextAttributes *)textAttributes
David@0
  1784
{
Evan@166
  1785
	for (NSString *arg in inArgs) {
David@0
  1786
		if ([arg caseInsensitiveCompare:@"dir"] == NSOrderedSame) {
David@0
  1787
			//Right to left, left to right handling
David@0
  1788
			NSString	*direction = [inArgs objectForKey:arg];
David@0
  1789
			
David@0
  1790
			if ([direction caseInsensitiveCompare:@"rtl"] == NSOrderedSame) {
David@0
  1791
				[textAttributes setWritingDirection:NSWritingDirectionRightToLeft];
David@0
  1792
				
David@0
  1793
			} else if ([direction caseInsensitiveCompare:@"ltr"] == NSOrderedSame) {
David@0
  1794
				[textAttributes setWritingDirection:NSWritingDirectionLeftToRight];
David@0
  1795
			}
David@0
  1796
			
David@0
  1797
		} else if ([arg caseInsensitiveCompare:@"class"] == NSOrderedSame) {
David@0
  1798
			NSString	*class = [inArgs objectForKey:arg];
David@0
  1799
			if ([class caseInsensitiveCompare:@"send"] == NSOrderedSame) {
David@0
  1800
				send = YES;
David@0
  1801
				receive = NO;
David@0
  1802
			} else if ([class caseInsensitiveCompare:@"receive"] == NSOrderedSame) {
David@0
  1803
				receive = YES;
David@0
  1804
				send = NO;
David@0
  1805
			} else if ([class caseInsensitiveCompare:@"status"] == NSOrderedSame) {
David@0
  1806
				[textAttributes setTextColor:[NSColor grayColor]];
David@0
  1807
			}
David@0
  1808
		}
David@0
  1809
	}
David@0
  1810
}
David@0
  1811
David@0
  1812
- (NSAttributedString *)processImgTagArgs:(NSDictionary *)inArgs attributes:(AITextAttributes *)textAttributes baseURL:(NSString *)inBaseURL
David@0
  1813
{
David@0
  1814
	NSAttributedString			*attachString;
David@1655
  1815
	AITextAttachmentExtension   *attachment = [[[AITextAttachmentExtension alloc] init] autorelease];
David@0
  1816
Evan@166
  1817
	for (NSString *arg in inArgs) {
David@0
  1818
		if ([arg caseInsensitiveCompare:@"src"] == NSOrderedSame) {
David@0
  1819
			NSString	*src = [inArgs objectForKey:arg];
Evan@633
  1820
			NSURL		*url;
Evan@633
  1821
			
Evan@633
  1822
			if ([src rangeOfString:@"://"].location != NSNotFound) {
Evan@633
  1823
				url = (baseURL ?
Evan@633
  1824
					   [NSURL URLWithString:src relativeToURL:[NSURL URLWithString:baseURL]] :
Evan@633
  1825
					   [NSURL URLWithString:src]);
Evan@633
  1826
			} else {
Evan@633
  1827
				url = [NSURL fileURLWithPath:(baseURL ?
Evan@633
  1828
											  [baseURL stringByAppendingPathComponent:src] :
Evan@633
  1829
											  src)];
Evan@633
  1830
			}
Evan@633
  1831
			
zacw@2236
  1832
			if (url && [url isFileURL]) {
Evan@366
  1833
				src = [url path];
Evan@366
  1834
				
Evan@366
  1835
				if (inBaseURL && ![[NSFileManager defaultManager] fileExistsAtPath:src])
Evan@366
  1836
					src = [inBaseURL stringByAppendingPathComponent:src];
zacw@2236
  1837
			} else {
zacw@2236
  1838
				return [NSAttributedString attributedStringWithLinkLabel:src linkDestination:src];
Evan@366
  1839
			}
David@0
  1840
David@0
  1841
			[attachment setPath:src];
David@0
  1842
		}
David@0
  1843
		if ([arg caseInsensitiveCompare:@"alt"] == NSOrderedSame) {
David@0
  1844
			[attachment setString:[inArgs objectForKey:arg]];
David@0
  1845
			[attachment setHasAlternate:YES];
David@0
  1846
		}
David@0
  1847
		if ([arg caseInsensitiveCompare:@"class"] == NSOrderedSame) {
David@0
  1848
			[attachment setImageClass:[inArgs objectForKey:arg]];
David@0
  1849
		}
David@0
  1850
	}
David@0
  1851
	
David@0
  1852
	[attachment setShouldSaveImageForLogging:YES];
David@0
  1853
David@0
  1854
	//Use the real image if possible
David@0
  1855
	NSImage		*image = [attachment image];
David@0
  1856
	
David@0
  1857
	//Otherwise, use an icon representing the image
David@0
  1858
	if (!image) image = [attachment iconImage];
David@0
  1859
David@0
  1860
	if (image) {
David@0
  1861
		NSTextAttachmentCell *cell = [[NSTextAttachmentCell alloc] initImageCell:image];
David@0
  1862
		[attachment setAttachmentCell:cell];
David@0
  1863
		[cell release];
David@0
  1864
		
David@0
  1865
		attachString = [NSAttributedString attributedStringWithAttachment:attachment];
David@0
  1866
	} else {
David@0
  1867
		attachString = nil;
David@0
  1868
	}
David@0
  1869
David@0
  1870
	return attachString;
David@0
  1871
}
David@0
  1872
David@0
  1873
/*!
David@0
  1874
 * @brief Append an image to the HTML
David@0
  1875
 *
David@0
  1876
 * @param attachmentImage The image itself
David@0
  1877
 * @param inPath The path at which the image is stored on disk, or nil if it is not currently on disk at a known location
David@0
  1878
 * @param string The HTML string in progress to which the image should be appended
David@0
  1879
 * @param inName The name of the image, used as the alt parameter and as the filename if necessary
David@0
  1880
 * @param imageClass A string which wlll be used as the 'class' parameter's value, or nil
David@0
  1881
 * @param imagesPath The path at which to write out the image if writing is necessary. May be nil if inPath is not nil.
David@0
  1882
 * @param uniqueifyHTML If YES, the resulting HTML will avoid all caching by using "?" followed by the date in seconds in the src tag.
David@0
  1883
 *
David@0
  1884
 * @result YES if successful.
David@0
  1885
 */
David@0
  1886
 - (BOOL)appendImage:(NSImage *)attachmentImage
David@0
  1887
			 atPath:(NSString *)inPath
David@0
  1888
		   toString:(NSMutableString *)string
David@0
  1889
		   withName:(NSString *)inName 
David@0
  1890
		 imageClass:(NSString *)imageClass
David@0
  1891
		 imagesPath:(NSString *)imagesPath
David@0
  1892
	  uniqueifyHTML:(BOOL)uniqueifyHTML
David@0
  1893
{	
David@0
  1894
	NSString	*shortFileName;
David@0
  1895
	BOOL		success = NO;
David@0
  1896
David@0
  1897
	if (imagesPath || !inPath) {
David@0
  1898
		//create the images directory if it doesn't exist
David@0
  1899
		if (imagesPath) {
David@472
  1900
			[[NSFileManager defaultManager] createDirectoryAtPath:imagesPath withIntermediateDirectories:YES attributes:nil error:NULL];
David@0
  1901
		}
David@0
  1902
David@0
  1903
		if (inPath) {
David@0
  1904
			//Just get it from the original path. This is especially good for emoticons.
David@0
  1905
			success = YES;
David@0
  1906
David@0
  1907
		} else {
David@0
  1908
			/* If we get here, the image is not on disk. If we don't have a path at which to save images,
David@0
  1909
			 * save to the temporary directory.
David@0
  1910
			 */
David@0
  1911
			if (!imagesPath) {
David@0
  1912
				imagesPath = NSTemporaryDirectory();
David@0
  1913
			}
David@0
  1914
David@0
  1915
			//Make sure the image has an appropriate extension
David@0
  1916
			if ([[inName pathExtension] caseInsensitiveCompare:@"png"] != NSOrderedSame) {
David@0
  1917
				inName = [inName stringByAppendingPathExtension:@"png"];
David@0
  1918
			}	
David@0
  1919
David@0
  1920
			shortFileName = [inName safeFilenameString];
David@0
  1921
			inPath = [[NSFileManager defaultManager] uniquePathForPath:[imagesPath stringByAppendingPathComponent:shortFileName]];
David@0
  1922
			NSData *pngRep = [attachmentImage PNGRepresentation];
David@0
  1923
			if([pngRep length] == 0) AILog(@"Couldn't get png representation for image %@", attachmentImage);
David@0
  1924
			success = [pngRep writeToFile:inPath atomically:YES];
David@0
  1925
		}
David@0
  1926
		
David@0
  1927
		if (!success) {
David@0
  1928
			NSLog(@"Failed to write image %@",inName);
David@0
  1929
		}
David@0
  1930
David@0
  1931
	} else {
David@0
  1932
		success = YES;
David@0
  1933
	}
David@0
  1934
	
David@0
  1935
	if (success) {
David@0
  1936
		NSString *srcPath = [[[NSURL fileURLWithPath:inPath] absoluteString] stringByEscapingForXMLWithEntities:nil];
David@0
  1937
		NSString *altName = (inName ? [inName stringByEscapingForXMLWithEntities:nil] : [srcPath lastPathComponent]);
David@0
  1938
David@0
  1939
		//Note the space at the end of the tag
David@0
  1940
		NSString *imageClassTag = (imageClass ? [NSString stringWithFormat:@"class=\"%@\" ", imageClass] : @"");
David@0
  1941
David@0
  1942
		if (attachmentImage) {
David@0
  1943
			//Include size information if possible
David@0
  1944
			NSSize imageSize = [attachmentImage size];
David@0
  1945
			[string appendFormat:@"<img %@src=\"%@%@\" alt=\"%@\" width=\"%i\" height=\"%i\">",
David@0
  1946
				imageClassTag,
David@0
  1947
				srcPath, (uniqueifyHTML ? [NSString stringWithFormat:@"?%i", [[NSDate date] timeIntervalSince1970]] : @""),
David@0
  1948
				altName,
David@0
  1949
				(int)imageSize.width, (int)imageSize.height];
David@0
  1950
David@0
  1951
		} else {
David@0
  1952
			[string appendFormat:@"<img %@src=\"%@%@\" alt=\"%@\">",
David@0
  1953
				imageClassTag,
David@0
  1954
				srcPath, (uniqueifyHTML ? [NSString stringWithFormat:@"?%i", [[NSDate date] timeIntervalSince1970]] : @""),
David@0
  1955
				altName];
David@0
  1956
		}
David@0
  1957
	}
David@0
  1958
David@0
  1959
	return success;
David@0
  1960
}
David@0
  1961
David@516
  1962
#pragma mark Properties
David@0
  1963
David@516
  1964
@synthesize XMLNamespace;
David@0
  1965
David@0
  1966
- (BOOL)generatesStrictXHTML
David@0
  1967
{
David@0
  1968
	return thingsToInclude.generateStrictXHTML;
David@0
  1969
}
David@0
  1970
- (void)setGeneratesStrictXHTML:(BOOL)newValue
David@0
  1971
{
David@0
  1972
	thingsToInclude.generateStrictXHTML = newValue;
David@0
  1973
}
David@0
  1974
David@0
  1975
- (BOOL)includesHeaders
David@0
  1976
{
David@0
  1977
	return thingsToInclude.headers;
David@0
  1978
}
David@0
  1979
- (void)setIncludesHeaders:(BOOL)newValue
David@0
  1980
{
David@0
  1981
	thingsToInclude.headers = newValue;
David@0
  1982
}
David@0
  1983
David@0
  1984
- (BOOL)includesFontTags
David@0
  1985
{
David@0
  1986
	return thingsToInclude.fontTags;
David@0
  1987
}
David@0
  1988
- (void)setIncludesFontTags:(BOOL)newValue
David@0
  1989
{
David@0
  1990
	thingsToInclude.fontTags = newValue;
David@0
  1991
}
David@0
  1992
David@0
  1993
- (BOOL)closesFontTags
David@0
  1994
{
David@0
  1995
	return thingsToInclude.closingFontTags;
David@0
  1996
}
David@0
  1997
- (void)setClosesFontTags:(BOOL)newValue
David@0
  1998
{
David@0
  1999
	thingsToInclude.closingFontTags = newValue;
David@0
  2000
}
David@0
  2001
David@0
  2002
- (BOOL)includesColorTags
David@0
  2003
{
David@0
  2004
	return thingsToInclude.colorTags;
David@0
  2005
}
David@0
  2006
- (void)setIncludesColorTags:(BOOL)newValue
David@0
  2007
{
David@0
  2008
	thingsToInclude.colorTags = newValue;
David@0
  2009
}
David@0
  2010
David@0
  2011
- (BOOL)includesStyleTags
David@0
  2012
{
David@0
  2013
	return thingsToInclude.styleTags;
David@0
  2014
}
David@0
  2015
- (void)setIncludesStyleTags:(BOOL)newValue
David@0
  2016
{
David@0
  2017
	thingsToInclude.styleTags = newValue;
David@0
  2018
}
David@0
  2019
David@0
  2020
- (BOOL)encodesNonASCII
David@0
  2021
{
David@0
  2022
	return thingsToInclude.nonASCII;
David@0
  2023
}
David@0
  2024
- (void)setEncodesNonASCII:(BOOL)newValue
David@0
  2025
{
David@0
  2026
	thingsToInclude.nonASCII = newValue;
David@0
  2027
}
David@0
  2028
David@0
  2029
- (BOOL)preservesAllSpaces
David@0
  2030
{
David@0
  2031
	return thingsToInclude.allSpaces;
David@0
  2032
}
David@0
  2033
- (void)setPreservesAllSpaces:(BOOL)newValue
David@0
  2034
{
David@0
  2035
	thingsToInclude.allSpaces = newValue;
David@0
  2036
}
David@0
  2037
David@0
  2038
- (BOOL)usesAttachmentTextEquivalents
David@0
  2039
{
David@0
  2040
	return thingsToInclude.attachmentTextEquivalents;
David@0
  2041
}
David@0
  2042
- (void)setUsesAttachmentTextEquivalents:(BOOL)newValue
David@0
  2043
{
David@0
  2044
	thingsToInclude.attachmentTextEquivalents = newValue;
David@0
  2045
}
David@0
  2046
David@0
  2047
- (BOOL)onlyConvertImageAttachmentsToIMGTagsWhenSendingAMessage
David@0
  2048
{
David@0
  2049
	return thingsToInclude.onlyIncludeOutgoingImages;
David@0
  2050
}
David@0
  2051
- (void)setOnlyConvertImageAttachmentsToIMGTagsWhenSendingAMessage:(BOOL)newValue
David@0
  2052
{
David@0
  2053
	thingsToInclude.onlyIncludeOutgoingImages = newValue;
David@0
  2054
}
David@0
  2055
David@0
  2056
- (BOOL)onlyUsesSimpleTags
David@0
  2057
{
David@0
  2058
	return thingsToInclude.simpleTagsOnly;
David@0
  2059
}
David@0
  2060
- (void)setOnlyUsesSimpleTags:(BOOL)newValue
David@0
  2061
{
David@0
  2062
	thingsToInclude.simpleTagsOnly = newValue;
David@0
  2063
}
David@0
  2064
David@0
  2065
- (BOOL)includesBodyBackground
David@0
  2066
{
David@0
  2067
	return thingsToInclude.bodyBackground;
David@0
  2068
}
David@0
  2069
- (void)setIncludesBodyBackground:(BOOL)newValue
David@0
  2070
{
David@0
  2071
	thingsToInclude.bodyBackground = newValue;
David@0
  2072
}
David@0
  2073
David@0
  2074
- (BOOL)allowAIMsubprofileLinks
David@0
  2075
{
David@0
  2076
	return thingsToInclude.allowAIMsubprofileLinks;
David@0
  2077
}
David@0
  2078
- (void)setAllowAIMsubprofileLinks:(BOOL)newValue
David@0
  2079
{
David@0
  2080
	thingsToInclude.allowAIMsubprofileLinks = newValue;
David@0
  2081
}
David@0
  2082
David@0
  2083
- (BOOL)allowJavascriptURLs
David@0
  2084
{
David@0
  2085
	return thingsToInclude.allowJavascriptURLs;
David@0
  2086
}
David@0
  2087
- (void)setAllowJavascriptURLs:(BOOL)newValue
David@0
  2088
{
David@0
  2089
	thingsToInclude.allowJavascriptURLs = newValue;
David@0
  2090
}
David@0
  2091
David@516
  2092
@synthesize baseURL;
David@0
  2093
David@0
  2094
@end
David@0
  2095
David@0
  2096
static AIHTMLDecoder *classMethodInstance = nil;
David@0
  2097
David@0
  2098
@implementation AIHTMLDecoder (ClassMethodCompatibility)
David@0
  2099
David@0
  2100
+ (AIHTMLDecoder *)classMethodInstance
David@0
  2101
{
David@0
  2102
	if (classMethodInstance == nil)
David@0
  2103
		classMethodInstance = [[self alloc] init];
David@0
  2104
	return classMethodInstance;
David@0
  2105
}
David@0
  2106
David@0
  2107
//For compatibility
David@0
  2108
+ (NSString *)encodeHTML:(NSAttributedString *)inMessage encodeFullString:(BOOL)encodeFullString
David@0
  2109
{
David@0
  2110
	[self classMethodInstance];
David@0
  2111
	classMethodInstance->thingsToInclude.headers = 
David@0
  2112
	classMethodInstance->thingsToInclude.fontTags = 
David@0
  2113
	classMethodInstance->thingsToInclude.closingFontTags = 
David@0
  2114
	classMethodInstance->thingsToInclude.colorTags = 
David@0
  2115
	classMethodInstance->thingsToInclude.nonASCII = 
David@0
  2116
	classMethodInstance->thingsToInclude.allSpaces = 
David@0
  2117
		encodeFullString;
David@0
  2118
	classMethodInstance->thingsToInclude.styleTags = 
David@0
  2119
	classMethodInstance->thingsToInclude.attachmentTextEquivalents = 
David@0
  2120
		YES;
David@0
  2121
	classMethodInstance->thingsToInclude.onlyIncludeOutgoingImages = 
David@0
  2122
	classMethodInstance->thingsToInclude.simpleTagsOnly = 
David@0
  2123
	classMethodInstance->thingsToInclude.bodyBackground =
David@0
  2124
	classMethodInstance->thingsToInclude.allowAIMsubprofileLinks =
David@0
  2125
		NO;
David@0
  2126
	
David@0
  2127
	return [classMethodInstance encodeHTML:inMessage imagesPath:nil];
David@0
  2128
}
David@0
  2129
David@0
  2130
// inMessage: AttributedString to encode
David@0
  2131
// headers: YES to include HTML and BODY tags
David@0
  2132
// fontTags: YES to include FONT tags
David@0
  2133
// closeFontTags: YES to close the font tags
David@0
  2134
// styleTags: YES to include B/I/U tags
David@0
  2135
// closeStyleTagsOnFontChange: YES to close and re-insert style tags when opening a new font tag
David@0
  2136
// encodeNonASCII: YES to encode non-ASCII characters as their HTML equivalents
David@0
  2137
// encodeSpaces: YES to preserve spacing when displaying the HTML in a web browser by converting multiple spaces and tabs to &nbsp codes.
David@0
  2138
// attachmentsAsText: YES to convert all attachments to their text equivalent if possible; NO to imbed <IMG SRC="...> tags
David@0
  2139
// onlyIncludeOutgoingImages: YES to only convert attachments to <IMG SRC="...> tags which should be sent to another user. Only relevant if attachmentsAsText is NO.
David@0
  2140
// simpleTagsOnly: YES to separate out FONT tags and include only the most basic HTML elements. Intended for protocols with minimal formatting support such as MSN
David@0
  2141
// bodyBackground: YES to set an Adium-internal attribute, AIBodyColorAttributeName, if there's a background. Used only for the message view.
David@0
  2142
// allowJavascriptURLs: NO to strip all URLs using the javascript: scheme so as to avoid people sending malicious links
David@0
  2143
+ (NSString *)encodeHTML:(NSAttributedString *)inMessage
David@0
  2144
				 headers:(BOOL)includeHeaders 
David@0
  2145
				fontTags:(BOOL)includeFontTags
David@0
  2146
	  includingColorTags:(BOOL)includeColorTags 
David@0
  2147
		   closeFontTags:(BOOL)closeFontTags
David@0
  2148
			   styleTags:(BOOL)includeStyleTags
David@0
  2149
closeStyleTagsOnFontChange:(BOOL)closeStyleTagsOnFontChange 
David@0
  2150
		  encodeNonASCII:(BOOL)encodeNonASCII
David@0
  2151
			encodeSpaces:(BOOL)encodeSpaces
David@0
  2152
			  imagesPath:(NSString *)imagesPath
David@0
  2153
	   attachmentsAsText:(BOOL)attachmentsAsText
David@0
  2154
onlyIncludeOutgoingImages:(BOOL)onlyIncludeOutgoingImages
David@0
  2155
		  simpleTagsOnly:(BOOL)simpleOnly
David@0
  2156
		  bodyBackground:(BOOL)bodyBackground
David@0
  2157
     allowJavascriptURLs:(BOOL)allowJS
David@0
  2158
{
David@0
  2159
#pragma unused(closeStyleTagsOnFontChange)
David@0
  2160
	[self classMethodInstance];
David@0
  2161
	classMethodInstance->thingsToInclude.headers = includeHeaders;
David@0
  2162
	classMethodInstance->thingsToInclude.fontTags = includeFontTags;
David@0
  2163
	classMethodInstance->thingsToInclude.closingFontTags = closeFontTags;
David@0
  2164
	classMethodInstance->thingsToInclude.colorTags = includeColorTags;
David@0
  2165
	classMethodInstance->thingsToInclude.styleTags = includeStyleTags;
David@0
  2166
	classMethodInstance->thingsToInclude.nonASCII = encodeNonASCII;
David@0
  2167
	classMethodInstance->thingsToInclude.allSpaces = encodeSpaces;
David@0
  2168
	classMethodInstance->thingsToInclude.attachmentTextEquivalents = attachmentsAsText;
David@0
  2169
	classMethodInstance->thingsToInclude.onlyIncludeOutgoingImages = onlyIncludeOutgoingImages;
David@0
  2170
	classMethodInstance->thingsToInclude.simpleTagsOnly = simpleOnly;
David@0
  2171
	classMethodInstance->thingsToInclude.bodyBackground = bodyBackground;
David@0
  2172
	classMethodInstance->thingsToInclude.allowAIMsubprofileLinks = NO;
David@0
  2173
	classMethodInstance->thingsToInclude.allowJavascriptURLs = allowJS;
David@0
  2174
David@0
  2175
	return [classMethodInstance encodeHTML:inMessage imagesPath:imagesPath];
David@0
  2176
}
David@0
  2177
David@0
  2178
+ (NSAttributedString *)decodeHTML:(NSString *)inMessage
David@0
  2179
{
David@0
  2180
	return [[self classMethodInstance] decodeHTML:inMessage withDefaultAttributes:nil];
David@0
  2181
}
David@0
  2182
David@0
  2183
+ (NSAttributedString *)decodeHTML:(NSString *)inMessage withDefaultAttributes:(NSDictionary *)inDefaultAttributes
David@0
  2184
{
David@0
  2185
	return [[self classMethodInstance] decodeHTML:inMessage withDefaultAttributes:inDefaultAttributes];
David@0
  2186
}
David@0
  2187
David@0
  2188
+ (NSDictionary *)parseArguments:(NSString *)arguments
David@0
  2189
{
David@0
  2190
	return [[self classMethodInstance] parseArguments:arguments];
David@0
  2191
}
David@0
  2192
David@0
  2193
@end
David@0
  2194
David@0
  2195
#pragma mark C functions
David@0
  2196
David@0
  2197
int HTMLEquivalentForFontSize(int fontSize)
David@0
  2198
{
David@0
  2199
	if (fontSize <= 9) {
David@0
  2200
		return 1;
David@0
  2201
	} else if (fontSize <= 10) {
David@0
  2202
		return 2;
David@0
  2203
	} else if (fontSize <= 12) {
David@0
  2204
		return 3;
David@0
  2205
	} else if (fontSize <= 14) {
David@0
  2206
		return 4;
David@0
  2207
	} else if (fontSize <= 18) {
David@0
  2208
		return 5;
David@0
  2209
	} else if (fontSize <= 24) {
David@0
  2210
		return 6;
David@0
  2211
	} else {
David@0
  2212
		return 7;
David@0
  2213
	}
David@0
  2214
}
David@0
  2215
David@0
  2216
@implementation NSString (AIHTMLDecoderAdditions)
David@0
  2217
David@0
  2218
/*!
David@0
  2219
 * @brief Allow absoluteString to be called on NSString objects
David@0
  2220
 *
David@0
  2221
 * This exists to work around an incompatibilty with older, buggy versions of Adium which would incorrectly set
David@0
  2222
 * an NSString for the NSLinkAttributeName attribute of an NSAttributedString.  This should always be an NUSRL.
David@0
  2223
 * Rather than figure out upgrade code in every possible lcoation, we just allow NSString to have absoluteString called
David@0
  2224
 * upon it, which is how we get the string value of NSURL objects.
David@0
  2225
 */
David@0
  2226
- (NSString *)absoluteString
David@0
  2227
{
David@0
  2228
	return self;
David@0
  2229
}
David@0
  2230
David@0
  2231
/*!
David@0
  2232
 * @brief Convert ASCII Symbol font to the appropriate Unicode characters
David@0
  2233
 *
David@0
  2234
 * This is needed because Windows Symbol font uses the normal ASCII range while OS X's uses Unicode properly.
David@0
  2235
 * A table extracted from http://www.alanwood.net/demos/symbol.html converts
David@0
  2236
 * Symbol characters in the 32 to 254 range into their Unicode equivalents (also in the Symbol font).
David@0
  2237
 *
David@0
  2238
 * Characters which can't be converted are replaced by ''.
David@0
  2239
 */
David@0
  2240
- (NSString *)stringByConvertingSymbolToSymbolUnicode
David@0
  2241
{
David@0
  2242
	NSMutableString		*decodedString = [NSMutableString string];
David@0
  2243
David@0
  2244
	//Symbol to Unicode Escape for characters 32 through 126 in the Symbol font
David@0
  2245
	static const char *lowSymbolTable[] = {
David@0
  2246
		" ", "", "\xE2\x88\x80", "", "\xE2\x88\x83", "", "", "\xE2\x88\x8D", "", "", "\xE2\x88\x97",
David@0
  2247
		"", "", "\xE2\x88\x92", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",
David@0
  2248
		"\xE2\x89\x85", "\xCE\x91", "\xCE\x92", "\xCE\xA7", "\xCE\x94", "\xCE\x95", "\xCE\xA6", "\xCE\x93",
David@0
  2249
		"\xCE\x97", "\xCE\x99", "\xCF\x91", "\xCE\x9A", "\xCE\x9B", "\xCE\x9C", "\xCE\x9D", "\xCE\x9F",
David@0
  2250
		"\xCE\xA0", "\xCE\x98", "\xCE\xA1", "\xCE\xA3", "\xCE\xA4", "\xCE\xA5", "\xCF\x82", "\xCE\xA9",
David@0
  2251
		"\xCE\x9E", "\xCE\xA8", "\xCE\x96", "", "\xE2\x88\xB4", "", "\xE2\x8A\xA5", "", "", "\xCE\xB1",
David@0
  2252
		"\xCE\xB2", "\xCF\x87", "\xCE\xB4", "\xCE\xB5", "\xCF\x86", "\xCE\xB3", "\xCE\xB7", "\xCE\xB9", "\xCF\x95",
David@0
  2253
		"\xCE\xBA", "\xCE\xBB", "\xCE\xBC", "\xCE\xBD", "\xCE\xBF", "\xCF\x80", "\xCE\xB8", "\xCF\x81", "\xCF\x83",
David@0
  2254
		"\xCF\x84", "\xCF\x85", "\xCF\x96", "\xCF\x89", "\xCE\xBE", "\xCF\x88", "\xCE\xB6", "", "", "", "\xE2\x88\xBC"	};
David@0
  2255
David@0
  2256
	//Symbol to Unicode Escape for characters 161 through 254 in the Symbol font
David@0
  2257
	static const char *highSymbolTable[] = {
David@0
  2258
		"\xCF\x92", "\xE2\x80\xB2", "\xE2\x89\xA4", "\xE2\x81\x84", "\xE2\x88\x9E", "\xC6\x92", "\xE2\x99\xA3",
David@0
  2259
		"\xE2\x99\xA6", "\xE2\x99\xA5", "\xE2\x99\xA0", "\xE2\x86\x94", "\xE2\x86\x90", "\xE2\x86\x91", "\xE2\x86\x92",
David@0
  2260
		"\xE2\x86\x93", "\xC2\xB1", "\xE2\x80\xB3", "\xE2\x89\xA5", "\xC3\x97", "\xE2\x88\x9D", "\xE2\x88\x82", "\xE2\x88\x99",
David@0
  2261
		"\xC3\xB7", "\xE2\x89\xA0", "\xE2\x89\xA1", "\xE2\x89\x88", "\xE2\x80\xA6", "\xE2\x8F\x90", "\xE2\x8E\xAF",
David@0
  2262
		"\xE2\x86\xB5", "\xE2\x84\xB5", "\xE2\x84\x91", "\xE2\x84\x9C", "\xE2\x84\x98", "\xE2\x8A\x97", "\xE2\x8A\x95",
David@0
  2263
		"\xE2\x88\x85", "\xE2\x88\xA9", "\xE2\x88\xAA", "\xE2\x8A\x83", "\xE2\x8A\x87", "\xE2\x8A\x84", "\xE2\x8A\x82",
David@0
  2264
		"\xE2\x8A\x86", "\xE2\x88\x88", "\xE2\x88\x89", "\xE2\x88\xA0", "\xE2\x88\x87", "\xC2\xAE", "\xC2\xA9", "\xE2\x84\xA2",
David@0
  2265
		"\xE2\x88\x8F", "\xE2\x88\x9A", "\xE2\x8B\x85", "\xC2\xAC", "\xE2\x88\xA7", "\xE2\x88\xA8", "\xE2\x87\x94",
David@0
  2266
		"\xE2\x87\x90", "\xE2\x87\x91", "\xE2\x87\x92", "\xE2\x87\x93", "\xE2\x97\x8A", "\xE2\x8C\xA9", "\xC2\xAE",
David@0
  2267
		"\xC2\xA9", "\xE2\x84\xA2", "\xE2\x88\x91", "\xE2\x8E\x9B", "\xE2\x8E\x9C", "\xE2\x8E\x9D", "\xE2\x8E\xA1", "\xE2\x8E\xA2",
David@0
  2268
		"\xE2\x8E\xA3", "\xE2\x8E\xA7", "\xE2\x8E\xA8", "\xE2\x8E\xA9", "\xE2\x8E\xAA", "\xE2\x82\xAC", "\xE2\x8C\xAA",
David@0
  2269
		"\xE2\x88\xAB", "\xE2\x8C\xA0", "\xE2\x8E\xAE", "\xE2\x8C\xA1", "\xE2\x8E\x9E", "\xE2\x8E\x9F", "\xE2\x8E\xA0",
David@0
  2270
		"\xE2\x8E\xA4", "\xE2\x8E\xA5", "\xE2\x8E\xA6", "\xE2\x8E\xAB", "\xE2\x8E\xAC", "\xE2\x8E\xAD"};
David@0
  2271
	
David@0
  2272
	NSData *utf8Data = [self dataUsingEncoding:NSUTF8StringEncoding];
David@0
  2273
	const char *utf8String = [utf8Data bytes];
David@0
  2274
	unsigned sourceLength = [utf8Data length];
David@0
  2275
	
David@0
  2276
	for (int i = 0; i < sourceLength; i++) {
David@0
  2277
		unichar	ch = utf8String[i];
David@0
  2278
		const char *replacement;
David@0
  2279
		
David@0
  2280
		if (ch >= 32 && ch <= 126) {
David@0
  2281
			replacement = lowSymbolTable[ch - 32];
David@0
  2282
David@0
  2283
		} else if (ch >= 161 && ch <= 254) {
David@0
  2284
			replacement = highSymbolTable[ch - 161];
David@0
  2285
David@0
  2286
		} else {
David@0
  2287
			replacement = NULL;
David@0
  2288
		}
David@0
  2289
David@0
  2290
		if (replacement && strlen(replacement)) {
David@0
  2291
			[decodedString appendString:[NSString stringWithUTF8String:replacement]];
David@0
  2292
David@0
  2293
		} else {
David@0
  2294
			[decodedString appendFormat:@"%c", ch];			
David@0
  2295
		}
David@0
  2296
	}
David@0
  2297
	
David@0
  2298
	return decodedString;	
David@0
  2299
}
David@0
  2300
David@0
  2301
/*!
David@0
  2302
 * @brief Convert Wingdings characters to their Unicode equivalents if possible
David@0
  2303
 *
David@0
  2304
 * This table extracted from http://www.alanwood.net/demos/wingdings.html attempts to convert
David@0
  2305
 * Wingdings characters into Unicode equivalents.  Characters which can't be converted are replaced by ''.
David@0
  2306
 */
David@0
  2307
- (NSString *)stringByConvertingWingdingsToUnicode
David@0
  2308
{	
David@0
  2309
	NSMutableString		*decodedString = [NSMutableString string];
David@0
  2310
David@0
  2311
	//Wingdings to Unicode Escape for characters 32 through 255 in the Wingdings font
David@0
  2312
	static const char *wingdingsTable[] = {
David@0
  2313
		" ", "\xE2\x9C\x8F", "\xE2\x9C\x82", "\xE2\x9C\x81", "", "", "", "",
David@0
  2314
		"\xE2\x98\x8E", "\xE2\x9C\x86", "\xE2\x9C\x89", "", "", "", "",
David@0
  2315
		"", "", "", "", "", "", "", "\xE2\x8C\x9B", "\xE2\x8C\xA8", "",
David@0
  2316
		"", "", "", "", "", "\xE2\x9C\x87", "\xE2\x9C\x8D", "", "\xE2\x9C\x8C",
David@0
  2317
		"", "", "", "\xE2\x98\x9C", "\xE2\x98\x9E", "\xE2\x98\x9D", "\xE2\x98\x9F",
David@0
  2318
		"", "\xE2\x98\xBA", "", "\xE2\x98\xB9", "", "\xE2\x98\xA0", "\xE2\x9A\x90",
David@0
  2319
		"", "\xE2\x9C\x88", "\xE2\x98\xBC", "", "\xE2\x9D\x84", "", "\xE2\x9C\x9E",
David@0
  2320
		"", "\xE2\x9C\xA0", "\xE2\x9C\xA1", "\xE2\x98\xAA", "\xE2\x98\xAF", "\xE0\xA5\x90",
David@0
  2321
		"\xE2\x98\xB8", "\xE2\x99\x88", "\xE2\x99\x89", "\xE2\x99\x8A", "\xE2\x99\x8B",
David@0
  2322
		"\xE2\x99\x8C", "\xE2\x99\x8D", "\xE2\x99\x8E", "\xE2\x99\x8F", "\xE2\x99\x90",
David@0
  2323
		"\xE2\x99\x91", "\xE2\x99\x92", "\xE2\x99\x93", "&", "&", "\xE2\x97\x8F", "\xE2\x9D\x8D",
David@0
  2324
		"\xE2\x96\xA0", "\xE2\x96\xA1", "", "\xE2\x9D\x91", "\xE2\x9D\x92", "", "\xE2\x99\xA6",
David@0
  2325
		"\xE2\x97\x86", "\xE2\x9D\x96", "", "\xE2\x8C\xA7", "\xE2\x8D\x93", "\xE2\x8C\x98", "\xE2\x9D\x80",
David@0
  2326
		"\xE2\x9C\xBF", "\xE2\x9D\x9D", "\xE2\x9D\x9E", "\xE2\x96\xAF", "\xE2\x93\xAA", "\xE2\x91\xA0",
David@0
  2327
		"\xE2\x91\xA1", "\xE2\x91\xA2", "\xE2\x91\xA3", "\xE2\x91\xA4", "\xE2\x91\xA5", "\xE2\x91\xA6",
David@0
  2328
		"\xE2\x91\xA7", "\xE2\x91\xA8", "\xE2\x91\xA9", "\xE2\x93\xBF", "\xE2\x9D\xB6", "\xE2\x9D\xB7",
David@0
  2329
		"\xE2\x9D\xB8", "\xE2\x9D\xB9", "\xE2\x9D\xBA", "\xE2\x9D\xBB", "\xE2\x9D\xBC", "\xE2\x9D\xBD",
David@0
  2330
		"\xE2\x9D\xBE", "\xE2\x9D\xBF", "", "", "", "", "", "", "", "", "\xC2\xB7", "\xE2\x80\xA2",
David@0
  2331
		"\xE2\x96\xAA", "\xE2\x97\x8B", "", "", "\xE2\x97\x89", "\xE2\x97\x8E", "", "\xE2\x96\xAA",
David@0
  2332
		"\xE2\x97\xBB", "", "\xE2\x9C\xA6", "\xE2\x98\x85", "\xE2\x9C\xB6", "\xE2\x9C\xB4", "\xE2\x9C\xB9",
David@0
  2333
		"\xE2\x9C\xB5", "", "\xE2\x8C\x96", "\xE2\x9C\xA7", "\xE2\x8C\x91", "", "\xE2\x9C\xAA", "\xE2\x9C\xB0",
David@0
  2334
		"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",
David@0
  2335
		"", "", "", "", "\xE2\x8C\xAB", "\xE2\x8C\xA6", "", "\xE2\x9E\xA2", "", "", "", "\xE2\x9E\xB2", "", "",
David@0
  2336
		"", "", "", "", "", "", "", "", "", "\xE2\x9E\x94", "", "", "", "", "", "", "\xE2\x87\xA6", "\xE2\x87\xA8",
David@0
  2337
		"\xE2\x87\xA7", "\xE2\x87\xA9", "\xE2\xAC\x84", "\xE2\x87\xB3", "\xE2\xAC\x80", "\xE2\xAC\x81", "\xE2\xAC\x83",
David@0
  2338
		"\xE2\xAC\x82", "\xE2\x96\xAD", "\xE2\x96\xAB", "\xE2\x9C\x97", "\xE2\x9C\x93", "\xE2\x98\x92", "\xE2\x98\x91", ""};
David@0
  2339
	
David@0
  2340
	NSData *utf8Data = [self dataUsingEncoding:NSUTF8StringEncoding];
David@0
  2341
	const char *utf8String = [utf8Data bytes];
David@0
  2342
	unsigned sourceLength = [utf8Data length];
David@0
  2343
	
David@0
  2344
	for (int i = 0; i < sourceLength; i++) {
David@0
  2345
		unichar	ch = utf8String[i];
David@0
  2346
		if (ch >= 32 && ch <= 255) {
David@0
  2347
			[decodedString appendString:[NSString stringWithUTF8String:wingdingsTable[ch - 32]]];
David@0
  2348
		} else {
David@0
  2349
			[decodedString appendFormat:@"%c", ch];
David@0
  2350
		}
David@0
  2351
	}
David@0
  2352
	
David@0
  2353
	return decodedString;
David@0
  2354
}
David@0
  2355
David@0
  2356
@end