Frameworks/Adium Framework/Source/AIHTMLDecoder.m
author Zachary West <zacw@adium.im>
Wed Oct 28 01:57:39 2009 -0400 (2009-10-28)
changeset 2660 6a8a206aa8af
parent 2578 c5b0c0b0c141
child 2667 3b3d1d6c5394
permissions -rw-r--r--
Address a few font issues that have cropped up recently. Fixes #12906.

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

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

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

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

I tried to approach this as "strip the background color when foreground isn't specified, go back to the way the old things were" but its behavior was just honestly confusing. Let's be consistent in some way.
David@0
     1
/*-------------------------------------------------------------------------------------------------------*\
David@0
     2
| Adium, 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@2660
   252
	NSString		*currentFamily = nil;
David@0
   253
	NSString		*currentColor = nil;
David@0
   254
	NSString		*currentBackColor = nil;
zacw@2660
   255
	int				 currentSize = 0;
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