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