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.
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.
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 \------------------------------------------------------------------------------------------------------ */
17 A quick and simple HTML to Attributed string converter (ha! --jmelloy)
20 #import <Adium/AIHTMLDecoder.h>
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>
32 #import <Adium/AITextAttachmentExtension.h>
33 #import <Adium/ESFileWrapperExtension.h>
34 #import <Adium/AIXMLElement.h>
36 #import <FriBidi/NSString-FBAdditions.h>
38 #import <CoreServices/CoreServices.h>
40 int HTMLEquivalentForFontSize(int fontSize);
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;
57 - (void)restoreAttributesFromDict:(NSDictionary *)inAttributes intoAttributes:(AITextAttributes *)textAttributes;
60 @interface NSString (AIHTMLDecoderAdditions)
61 - (NSString *)stringByConvertingWingdingsToUnicode;
62 - (NSString *)stringByConvertingSymbolToSymbolUnicode;
65 @implementation AIHTMLDecoder
67 static NSString *horizontalRule = nil;
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
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'
80 horizontalRule = [[NSString alloc] initWithCharacters:separatorUTF16 length:HORIZONTAL_RULE_LENGTH];
85 [XMLNamespace release]; XMLNamespace = nil;
86 [baseURL release]; baseURL = nil;
90 + (AIHTMLDecoder *)decoder
92 return [[[self alloc] init] autorelease];
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
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;
122 thingsToInclude.allowAIMsubprofileLinks = NO;
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
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];
155 #pragma mark Work methods
158 * @brief Parse arguments in a string
160 * The arguments are returned in an NSDictionary whose keys are all-lowercase
162 - (NSDictionary *)parseArguments:(NSString *)arguments
164 NSMutableDictionary *argDict;
166 static NSCharacterSet *equalsSet = nil,
170 NSString *key = nil, *value = nil;
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];
178 scanner = [NSScanner scannerWithString:arguments];
179 argDict = [NSMutableDictionary dictionary];
181 while (![scanner isAtEnd]) {
182 BOOL validKey, validValue;
185 validKey = [scanner scanUpToCharactersFromSet:equalsSet intoString:&key];
186 [scanner scanCharactersFromSet:equalsSet intoString:nil];
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];
196 validValue = [scanner scanUpToCharactersFromSet:spaceSet intoString:&value];
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]];
208 - (NSString *)encodeLooseHTML:(NSAttributedString *)inMessage imagesPath:(NSString *)imagesSavePath
210 NSFontManager *fontManager = [NSFontManager sharedFontManager];
212 NSColor *pageColor = nil;
213 BOOL openFontTag = NO;
215 //Setup the incoming message as a regular string, and get its length
216 NSString *inMessageString = [inMessage string];
217 unsigned messageLength = [inMessageString length];
219 //Setup the destination HTML string
220 NSMutableString *string = [NSMutableString string];
221 if (thingsToInclude.headers) {
222 [string appendString:@"<HTML>"];
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];
232 //If that doesn't work, try using the writing direction of the input field
233 if (dir == NSWritingDirectionNatural) {
234 dir = [[inMessage attribute:NSParagraphStyleAttributeName
236 effectiveRange:nil] baseWritingDirection];
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];
244 if (dir == NSWritingDirectionRightToLeft) {
245 [string appendString:@"<DIV dir=\"rtl\">"];
251 //Setup the default attributes
252 NSString *currentFamily = nil;
253 NSString *currentColor = nil;
254 NSString *currentBackColor = nil;
256 BOOL currentItalic = NO;
257 BOOL currentBold = NO;
258 BOOL currentUnderline = NO;
259 BOOL currentStrikethrough = NO;
260 NSString *link = nil;
261 NSString *oldLink = nil;
263 //Append the body tag (If there is a background color)
264 if (thingsToInclude.headers &&
265 (messageLength > 0) &&
266 (pageColor = [inMessage attribute:AIBodyColorAttributeName
268 effectiveRange:nil])) {
269 [string appendString:@"<BODY BGCOLOR=\"#"];
270 [string appendString:[pageColor hexString]];
271 [string appendString:@"\">"];
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];
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);
290 link = [[attributes objectForKey:NSLinkAttributeName] absoluteString];
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
294 (!link || (([link length] != 0) && ![oldLink isEqualToString:link]))) {
297 [string appendString:@"</a>"];
301 NSMutableString *chunk = [[inMessageString substringWithRange:searchRange] mutableCopy];
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) {
310 //Close any existing font tags, and open a new one
311 if (thingsToInclude.closingFontTags && openFontTag) {
312 [string appendString:@"</FONT>"];
314 if (!thingsToInclude.simpleTagsOnly) {
316 //Leave the <FONT open since we'll add the rest of the font tag on below
317 [string appendString:@"<FONT"];
321 if (thingsToInclude.fontTags && familyName && (![familyName isEqualToString:currentFamily] || thingsToInclude.closingFontTags)) {
322 if (thingsToInclude.simpleTagsOnly) {
323 [string appendString:[NSString stringWithFormat:@"<FONT FACE=\"%@\">",familyName]];
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]];
331 [string appendString:[NSString stringWithFormat:@" FACE=\"%@\"",familyName]];
335 [currentFamily release]; currentFamily = [familyName retain];
339 if (thingsToInclude.fontTags && font && !thingsToInclude.simpleTagsOnly) {
340 [string appendString:[NSString stringWithFormat:@" ABSZ=%i SIZE=%i", (int)pointSize, HTMLEquivalentForFontSize((int)pointSize)]];
341 currentSize = pointSize;
346 if (thingsToInclude.simpleTagsOnly) {
347 [string appendString:[NSString stringWithFormat:@"<FONT COLOR=\"#%@\">",color]];
349 [string appendString:[NSString stringWithFormat:@" COLOR=\"#%@\"",color]];
352 //Background Color per tag
354 if (!thingsToInclude.simpleTagsOnly) {
355 [string appendString:[NSString stringWithFormat:@" BACK=\"#%@\"",backColor]];
359 if (color != currentColor) {
360 currentColor = color;
363 if (backColor != currentBackColor) {
364 currentBackColor = backColor;
367 //Close the font tag if necessary
368 if (!thingsToInclude.simpleTagsOnly) {
369 [string appendString:@">"];
373 //Style (Bold, italic, underline, strikethrough)
374 if (thingsToInclude.styleTags) {
375 if (currentItalic && !isItalic) {
376 [string appendString:@"</I>"];
378 } else if (!currentItalic && isItalic) {
379 [string appendString:@"<I>"];
383 if (currentUnderline && !hasUnderline) {
384 [string appendString:@"</U>"];
385 currentUnderline = NO;
386 } else if (!currentUnderline && hasUnderline) {
387 [string appendString:@"<U>"];
388 currentUnderline = YES;
391 if (currentBold && !isBold) {
392 [string appendString:@"</B>"];
394 } else if (!currentBold && isBold) {
395 [string appendString:@"<B>"];
399 if (currentStrikethrough && !hasStrikethrough) {
400 [string appendString:@"</S>"];
401 currentStrikethrough = NO;
402 } else if (!currentStrikethrough && hasStrikethrough) {
403 [string appendString:@"<S>"];
404 currentStrikethrough = YES;
409 if (!oldLink && link && [link length] != 0) {
410 NSString *linkString = ([link isKindOfClass:[NSURL class]] ? [(NSURL *)link absoluteString] : link);
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.
416 if (thingsToInclude.allowJavascriptURLs || [linkString rangeOfString:@"javascript"].location > 0) {
418 [string appendString:@"<a href=\""];
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
425 if (thingsToInclude.allowAIMsubprofileLinks &&
426 ([linkString rangeOfString:@"%25n"].location != NSNotFound)) {
427 NSMutableString *fixedLinkString = [[linkString mutableCopy] autorelease];
428 [fixedLinkString replaceOccurrencesOfString:@"%25n"
430 options:NSLiteralSearch
431 range:NSMakeRange(0, [fixedLinkString length])];
432 linkString = fixedLinkString;
435 [string appendString:[linkString stringByEscapingForXMLWithEntities:nil]];
436 if (!thingsToInclude.simpleTagsOnly) {
437 [string appendString:@"\" title=\""];
438 [string appendString:[linkString stringByEscapingForXMLWithEntities:nil]];
441 NSString *classString = [attributes objectForKey:AIElementClassAttributeName];
443 if (!thingsToInclude.simpleTagsOnly && classString) {
444 [string appendString:@"\" class=\""];
445 [string appendString:classString];
448 [string appendString:@"\">"];
450 oldLink = linkString;
456 if ([attributes objectForKey:NSAttachmentAttributeName]) {
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];
463 if (textAttachment) {
464 if (![textAttachment isKindOfClass:[AITextAttachmentExtension class]]) {
465 NSLog(@"Message %@ gave an NSTextAttachment %@ - why is it not an AITextAttachmentExtension?",
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
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.
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.
484 BOOL shouldIncludeImageWithoutSaving = (!thingsToInclude.attachmentTextEquivalents &&
485 (!thingsToInclude.onlyIncludeOutgoingImages || (![attachment respondsToSelector:@selector(shouldAlwaysSendAsText)] ||
486 ![attachment shouldAlwaysSendAsText])));
487 BOOL appendedImage = NO;
489 if (shouldSaveImage || shouldIncludeImageWithoutSaving) {
490 NSString *existingPath, *imageName;
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)] :
497 imageName = [attachment string];
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.
505 if(!existingPath || ![[existingPath pathExtension] isEqualToString:@"pdf"])
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)];
513 if (existingPath || image) {
514 appendedImage = [self appendImage:image
518 imageClass:[attachment imageClass]
519 imagesPath:(shouldIncludeImageWithoutSaving ? imagesSavePath : nil)
520 uniqueifyHTML:shouldSaveImage];
522 //We were succesful appending the image tag, so release this chunk
523 [chunk release]; chunk = nil;
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];
534 [chunk release]; chunk = nil;
542 unsigned int replacements;
544 //Escape special HTML characters.
545 fullRange = NSMakeRange(0, [chunk length]);
547 replacements = [chunk replaceOccurrencesOfString:@"&" withString:@"&"
548 options:NSLiteralSearch range:fullRange];
549 fullRange.length += (replacements * 4);
551 replacements = [chunk replaceOccurrencesOfString:@"<" withString:@"<"
552 options:NSLiteralSearch range:fullRange];
553 fullRange.length += (replacements * 3);
555 replacements = [chunk replaceOccurrencesOfString:@">" withString:@">"
556 options:NSLiteralSearch range:fullRange];
557 fullRange.length += (replacements * 3);
560 replacements = [chunk replaceOccurrencesOfString:horizontalRule withString:@"<HR>"
561 options:NSLiteralSearch range:fullRange];
563 fullRange.length = [chunk length];
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.
570 replacements = [chunk replaceOccurrencesOfString:@"\t"
571 withString:@" "
572 options:NSLiteralSearch
574 fullRange.length += (replacements * 18);
576 //If the first character is a space, replace that leading ' ' with " " to preserve formatting.
577 if ([chunk hasPrefix:@" "]) {
578 [chunk replaceCharactersInRange:NSMakeRange(0, 1)
579 withString:@" "];
580 fullRange.length += 5;
583 /* Replace all remaining blocks of " " (<space><space>) with " " (<space>< >) so that
584 * formatting of large blocks of spaces in the middle of a line is preserved,
585 * and so WebKit properly line-wraps.
587 [chunk replaceOccurrencesOfString:@" "
588 withString:@" "
589 options:NSLiteralSearch
593 /* Encode line endings */
595 //Handle \r\n as a single line break
596 [chunk replaceOccurrencesOfString:@"\r\n"
598 options:NSLiteralSearch
601 //Now replace every remaining line ending
602 NSRange lineEndingRange;
603 while ((lineEndingRange = [chunk rangeOfLineBreakCharacter]).location != NSNotFound) {
604 [chunk replaceCharactersInRange:lineEndingRange
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.
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) {
617 [string appendFormat:@"&#x%x;", currentChar];
619 } else if (currentChar >= 127) {
620 if (!UCIsSurrogateHighCharacter(currentChar)) {
621 [string appendFormat:@"&#x%x;", currentChar];
624 //currentChar is the high character of a surrogate pair.
625 unichar lowSurrogate = 0xFFFF;
626 if ((i + 1) < length) {
627 lowSurrogate = [chunk characterAtIndex:++i];
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);
635 UnicodeScalarValue codePoint = UCGetUnicodeScalarValueForSurrogatePair(/*highSurrogate*/ currentChar, lowSurrogate);
636 [string appendFormat:@"&#x%x;", codePoint];
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;
649 [string appendString:chunk];
656 searchRange.location += searchRange.length;
659 [currentFamily release];
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>"];
669 //If we had a link on the last pass, close the link tag
672 [string appendString:@"</a>"];
676 if (thingsToInclude.fontTags && thingsToInclude.closingFontTags && openFontTag) {
677 //Close any open font tag
678 [string appendString:@"</FONT>"];
682 [string appendString:@"</DIV>"];
684 if (thingsToInclude.headers && pageColor) {
686 [string appendString:@"</BODY>"];
688 if (thingsToInclude.headers) {
690 [string appendString:@"</HTML>"];
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:@"\""
701 options:NSLiteralSearch
702 range:NSMakeRange(0, [string length])];
709 - (AIXMLElement *)elementWithAppKitAttributes:(NSDictionary *)attributes
710 attributeNames:(NSSet *)attributeNames
711 elementContent:(NSMutableString *)elementContent
712 shouldAddElementContentToTopElement:(out BOOL *)outAddElementContentToTopElement
713 imagesPath:(NSString *)imagesPath
715 if (!(attributes && [attributes count] && attributeNames && [attributeNames count]))
718 attributes = [attributes dictionaryWithIntersectionWithSetOfKeys:attributeNames];
720 NSString *linkValue = [attributes objectForKey:NSLinkAttributeName];
721 NSTextAttachment *attachmentValue = [attributes objectForKey:NSAttachmentAttributeName];
723 NSString *elementName = linkValue ? @"a" : @"span";
724 BOOL moreThanJustAnImage = [attributes count] - (attachmentValue != nil);
726 BOOL addElementContentToTopElement = YES;
727 AIXMLElement *thisElement = moreThanJustAnImage ? [AIXMLElement elementWithNamespaceName:XMLNamespace elementName:elementName] : nil;
729 [thisElement setValue:linkValue forAttribute:@"href"];
732 if (attachmentValue) {
733 AITextAttachmentExtension *extension;
734 if ([attachmentValue isKindOfClass:[AITextAttachmentExtension class]])
735 extension = (AITextAttachmentExtension *)attachmentValue;
737 extension = [AITextAttachmentExtension textAttachmentExtensionFromTextAttachment:attachmentValue];
739 if ((thingsToInclude.attachmentTextEquivalents ||
741 ([extension respondsToSelector:@selector(shouldAlwaysSendAsText)] && [extension shouldAlwaysSendAsText])) &&
742 ([extension respondsToSelector:@selector(string)])) {
743 [elementContent setString:[extension string]];
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];
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"];
754 NSString *path = [extension path];
756 NSString *destinationPath = [imagesPath stringByAppendingPathComponent:[path lastPathComponent]];
757 if ([[NSFileManager defaultManager] copyItemAtPath:path
758 toPath:destinationPath
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).
764 [imageElement setValue:[path lastPathComponent]
765 forAttribute:@"src"];
767 AILogWithSignature(@"Could not copy %@ to %@", path, destinationPath);
771 if (elementContent && [elementContent length]) {
772 [imageElement setValue:elementContent forAttribute:@"alt"];
776 [thisElement addObject:imageElement];
778 thisElement = imageElement;
781 addElementContentToTopElement = NO;
785 NSString *CSSString = [NSAttributedString CSSStringForTextAttributes:attributes];
786 if (CSSString && [CSSString length]) {
787 [thisElement setValue:CSSString forAttribute:@"style"];
790 if (outAddElementContentToTopElement) {
791 *outAddElementContentToTopElement = addElementContentToTopElement;
796 - (NSDictionary *)attributesByReplacingNSFontAttributeNameWithAIFontAttributeNames:(NSDictionary *)attributes
798 NSFont *font = [[attributes objectForKey:NSFontAttributeName] retain];
800 return [[attributes retain] autorelease];
802 NSMutableDictionary *mutableAttributes = [attributes mutableCopy];
804 [mutableAttributes removeObjectForKey:NSFontAttributeName];
806 [mutableAttributes setObject:[font familyName]
807 forKey:AIFontFamilyAttributeName];
808 [mutableAttributes setObject:[NSString stringWithFormat:@"%@pt", [NSString stringWithFloat:[font pointSize] maxDigits:2]]
809 forKey:AIFontSizeAttributeName];
811 NSFontTraitMask traits = [[NSFontManager sharedFontManager] traitsOfFont:font];
812 if (traits & NSBoldFontMask) {
813 [mutableAttributes setObject:@"bold" forKey:AIFontWeightAttributeName];
815 if (traits & NSItalicFontMask) {
816 [mutableAttributes setObject:@"italic" forKey:AIFontStyleAttributeName];
821 NSDictionary *result = [NSDictionary dictionaryWithDictionary:mutableAttributes];
822 [mutableAttributes release];
827 - (AIXMLElement *)rootStrictXHTMLElementForAttributedString:(NSAttributedString *)inMessage imagesPath:(NSString *)imagesSavePath
831 //Setup the incoming message as a regular string, and get its length
832 NSString *inMessageString = [inMessage string];
833 unsigned messageLength = [inMessageString length];
835 NSSet *emptySet = [NSSet set];
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];
841 //Root element: includeHeaders ? <html> : <div>
843 if (thingsToInclude.headers) {
844 [elementStack addObject:[AIXMLElement elementWithNamespaceName:XMLNamespace elementName:@"html"]];
845 [attributeNamesStack addObject:emptySet];
847 AIXMLElement *bodyElement = [AIXMLElement elementWithNamespaceName:XMLNamespace elementName:@"body"];
848 [[elementStack lastObject] addObject:bodyElement];
849 [elementStack addObject:bodyElement];
850 [attributeNamesStack addObject:emptySet];
853 if ((messageLength > 0) &&
854 (pageColor = [inMessage attribute:AIBodyColorAttributeName
856 effectiveRange:NULL]))
858 [bodyElement setValue:[@"background-color: " stringByAppendingString:[pageColor CSSRepresentation]] forAttribute:@"style"];
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
867 effectiveRange:nil] baseWritingDirection] == NSWritingDirectionRightToLeft))
869 [divElement setValue:@"rtl" forAttribute:@"dir"];
871 [[elementStack lastObject] addObject:divElement];
872 [elementStack addObject:divElement];
873 [attributeNamesStack addObject:emptySet];
875 NSMutableSet *CSSCapableAttributes = [[[NSAttributedString CSSCapableAttributesSet] mutableCopy] autorelease];
876 [CSSCapableAttributes addObject:NSLinkAttributeName];
877 NSSet *CSSCapableAttributesWithNoAttachment = [NSSet setWithSet:CSSCapableAttributes];
878 [CSSCapableAttributes addObject:NSAttachmentAttributeName];
880 NSDictionary *prevAttributes = nil;
881 //Loop through the entire string, handling each attribute run.
882 searchRange = NSMakeRange(0,messageLength);
883 while (searchRange.location < messageLength) {
885 NSDictionary *attributes = [self attributesByReplacingNSFontAttributeNameWithAIFontAttributeNames:[inMessage attributesAtIndex:searchRange.location
886 longestEffectiveRange:&runRange
887 inRange:searchRange]];
888 attributes = [attributes dictionaryWithIntersectionWithSetOfKeys:CSSCapableAttributes];
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
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;
908 NSMutableSet *attributeNames = [attributeNamesStack objectAtIndex:popRange.location];
909 NSMutableSet *intersection = [[attributeNames mutableCopy] autorelease];
910 [intersection intersectSet:mutableEndedKeys];
912 [attributeNames minusSet:intersection];
913 [attributesToRestore unionSet:attributeNames];
915 [mutableEndedKeys minusSet:intersection];
917 [attributeNamesStack removeObjectsInRange:popRange];
918 [elementStack removeObjectsInRange:popRange];
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
927 shouldAddElementContentToTopElement:NULL
928 imagesPath:imagesSavePath];
929 [[elementStack lastObject] addObject:restoreElement];
930 [elementStack addObject:restoreElement];
932 [attributeNamesStack addObject:attributesToRestore];
936 [mutableEndedKeys release];
939 //Now handle attributes that have started or changed.
940 NSMutableString *elementContent = [[[inMessageString substringWithRange:runRange] mutableCopy] autorelease];
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];
955 NSMutableArray *item = [[NSMutableArray alloc] initWithCapacity:2];
956 [item addObject:[NSNumber numberWithUnsignedInt:attributeRange.length]];
957 [item addObject:attributeName];
958 [startedKeysArray replaceObjectAtIndex:i withObject:item];
961 //Sort. Items will be sorted first by length, then by attribute name.
962 [startedKeysArray sortUsingSelector:@selector(compare:)];
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]];
969 //Eat any equal keys that follow.
973 && ([[[startedKeysArray objectAtIndex:i] objectAtIndex:0] unsignedIntValue] == [[[startedKeysArray objectAtIndex:j] objectAtIndex:0] unsignedIntValue])
975 [itemKeys addObject:[[startedKeysArray objectAtIndex:j] objectAtIndex:1]];
976 [startedKeysArray removeObjectAtIndex:j];
980 [[startedKeysArray objectAtIndex:i] replaceObjectAtIndex:1 withObject:itemKeys];
983 //Turn each consolidated bunch of keys into an element.
984 addElementContentToTopElement = NO;
986 for (NSArray *item in startedKeysArray) {
987 NSSet *itemKeys = [item objectAtIndex:1];
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];
996 [[elementStack lastObject] addObject:thisElement];
997 [attributeNamesStack addObject:itemKeys];
998 [elementStack addObject:thisElement];
999 } else if(!addElementContentToTopElement) {
1000 [[elementStack lastObject] addObject:elementContent];
1003 [startedKeysArray release];
1006 addElementContentToTopElement = YES;
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];
1015 //Add these zero or more lines, with BRs between them, to the top element on the stack.
1016 [[elementStack lastObject] addObjectsFromArray:linesAndBRs];
1019 searchRange.location += runRange.length;
1020 searchRange.length -= runRange.length;
1023 return [elementStack objectAtIndex:0];
1026 - (NSString *)encodeStrictXHTML:(NSAttributedString *)inMessage imagesPath:(NSString *)imagesSavePath
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];
1037 - (NSString *)encodeHTML:(NSAttributedString *)inMessage imagesPath:(NSString *)imagesSavePath
1039 return thingsToInclude.generateStrictXHTML ? [self encodeStrictXHTML:inMessage imagesPath:imagesSavePath] : [self encodeLooseHTML:inMessage imagesPath:imagesSavePath];
1042 - (NSAttributedString *)decodeHTML:(NSString *)inMessage
1044 return [self decodeHTML:inMessage withDefaultAttributes:nil];
1047 - (NSAttributedString *)decodeHTML:(NSString *)inMessage withDefaultAttributes:(NSDictionary *)inDefaultAttributes
1049 if (!inMessage) return [[[NSAttributedString alloc] init] autorelease];
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];
1060 //Reset the div and span ivars
1066 if (inDefaultAttributes) {
1067 textAttributes = [AITextAttributes textAttributesWithDictionary:inDefaultAttributes];
1069 textAttributes = [[[AITextAttributes alloc] init] autorelease];
1072 attrString = [[NSMutableAttributedString alloc] init];
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];
1079 scanner = [NSScanner scannerWithString:inMessage];
1080 [scanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@""]];
1083 while (![scanner isAtEnd]) {
1085 * Scan up to an HTML tag or escaped character.
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.
1090 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1091 if ([scanner scanUpToCharactersFromSet:tagCharStart intoString:&chunkString]) {
1092 id languageValue = [textAttributes languageValue];
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.
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.
1103 * Similarly, Webdings can be represented in the font Webdings in the Private Use unicode space.
1105 if (languageValue && ([languageValue intValue] == 143)) {
1106 NSString *fontFamily = [textAttributes fontFamily];
1108 if ([fontFamily caseInsensitiveCompare:@"Symbol"] == NSOrderedSame) {
1109 chunkString = [chunkString stringByConvertingSymbolToSymbolUnicode];
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];
1117 chunkString = [chunkString stringByConvertingWingdingsToUnicode];
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];
1128 [attrString appendString:chunkString withAttributes:[textAttributes dictionary]];
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)
1135 if ([tagOpen isEqualToString:@"<"]) { // HTML <tag>
1136 BOOL validTag = [scanner scanUpToCharactersFromSet:tagEnd intoString:&chunkString]; //Get the tag
1137 NSString *charactersToSkipAfterThisTag = nil;
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];
1145 } else if ([chunkString caseInsensitiveCompare:@"/HTML"] == NSOrderedSame) {
1147 [pool release]; pool = nil;
1150 //PRE -- ignore attributes for logViewer
1151 } else if ([chunkString caseInsensitiveCompare:@"PRE"] == NSOrderedSame ||
1152 [chunkString caseInsensitiveCompare:@"/PRE"] == NSOrderedSame) {
1154 [scanner scanUpToCharactersFromSet:absoluteTagEnd intoString:&chunkString];
1156 //XXX what's going on here?
1157 [textAttributes setTextColor:[NSColor blackColor]];
1160 } else if ([chunkString caseInsensitiveCompare:@"DIV"] == NSOrderedSame) {
1161 if ([scanner scanUpToCharactersFromSet:absoluteTagEnd
1162 intoString:&chunkString]) {
1163 [self processDivTagArgs:[self parseArguments:chunkString] attributes:textAttributes];
1167 } else if ([chunkString caseInsensitiveCompare:@"/DIV"] == NSOrderedSame) {
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
1177 } else if ([chunkString caseInsensitiveCompare:@"/A"] == NSOrderedSame) {
1178 [textAttributes setLinkURL:nil];
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
1186 } else if (([chunkString caseInsensitiveCompare:@"/BODY"] == NSOrderedSame) ||
1187 ([chunkString caseInsensitiveCompare:@"BODY/"] == NSOrderedSame)) {
1191 } else if ([chunkString caseInsensitiveCompare:@"FONT"] == NSOrderedSame) {
1192 if ([scanner scanUpToCharactersFromSet:absoluteTagEnd intoString:&chunkString]) {
1193 NSDictionary *changedAttributes;
1195 //Process the font tag's contents
1196 changedAttributes = [self processFontTagArgs:[self parseArguments:chunkString] attributes:textAttributes];
1197 [fontTagChangedAttributesQueue addObject:(changedAttributes ? changedAttributes : [NSDictionary dictionary])];
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)];
1208 } else if ([chunkString caseInsensitiveCompare:@"SPAN"] == NSOrderedSame) {
1209 if ([scanner scanUpToCharactersFromSet:absoluteTagEnd intoString:&chunkString]) {
1210 NSDictionary *changedAttributes;
1212 changedAttributes = [self processSpanTagArgs:[self parseArguments:chunkString] attributes:textAttributes];
1213 [spanTagChangedAttributesQueue addObject:(changedAttributes ? changedAttributes : [NSDictionary dictionary])];
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)];
1224 } else if ([chunkString caseInsensitiveCompare:@"BR"] == NSOrderedSame ||
1225 [chunkString caseInsensitiveCompare:@"BR/"] == NSOrderedSame ||
1226 [chunkString caseInsensitiveCompare:@"/BR"] == NSOrderedSame) {
1227 [attrString appendString:@"\n" withAttributes:nil];
1229 /* Make sure the tag closes; it may have a <BR /> which stopped the scanner at
1230 * at the space rather than the '>'
1232 [scanner scanUpToCharactersFromSet:absoluteTagEnd intoString:&chunkString];
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"
1238 charactersToSkipAfterThisTag = @"\n\r";
1241 } else if ([chunkString caseInsensitiveCompare:@"B"] == NSOrderedSame) {
1242 [textAttributes enableTrait:NSBoldFontMask];
1243 } else if ([chunkString caseInsensitiveCompare:@"/B"] == NSOrderedSame) {
1244 [textAttributes disableTrait:NSBoldFontMask];
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];
1253 } else if ([chunkString caseInsensitiveCompare:@"I"] == NSOrderedSame) {
1254 [textAttributes enableTrait:NSItalicFontMask];
1255 } else if ([chunkString caseInsensitiveCompare:@"/I"] == NSOrderedSame) {
1256 [textAttributes disableTrait:NSItalicFontMask];
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];
1265 } else if ([chunkString caseInsensitiveCompare:@"U"] == NSOrderedSame) {
1266 [textAttributes setUnderline:YES];
1267 } else if ([chunkString caseInsensitiveCompare:@"/U"] == NSOrderedSame) {
1268 [textAttributes setUnderline:NO];
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];
1277 } else if ([chunkString caseInsensitiveCompare:@"SUB"] == NSOrderedSame) {
1278 [textAttributes setSubscript:YES];
1279 } else if ([chunkString caseInsensitiveCompare:@"/SUB"] == NSOrderedSame) {
1280 [textAttributes setSubscript:NO];
1283 } else if ([chunkString caseInsensitiveCompare:@"SUP"] == NSOrderedSame) {
1284 [textAttributes setSuperscript:YES];
1285 } else if ([chunkString caseInsensitiveCompare:@"/SUP"] == NSOrderedSame) {
1286 [textAttributes setSuperscript:NO];
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
1295 [attrString appendAttributedString:attachString];
1298 } else if ([chunkString caseInsensitiveCompare:@"/IMG"] == NSOrderedSame) {
1299 //just ignore </img> if we find it
1302 } else if ([chunkString caseInsensitiveCompare:@"HR"] == NSOrderedSame) {
1303 [attrString appendString:horizontalRule withAttributes:nil];
1305 // Ignore <p> for those wacky AIM express users
1306 } else if ([chunkString caseInsensitiveCompare:@"P"] == NSOrderedSame ||
1307 ([chunkString caseInsensitiveCompare:@"/P"] == NSOrderedSame)) {
1309 // Ignore <head> tags
1310 } else if ([chunkString caseInsensitiveCompare:@"HEAD"] == NSOrderedSame ||
1311 ([chunkString caseInsensitiveCompare:@"/HEAD"] == NSOrderedSame)) {
1312 [scanner scanUpToCharactersFromSet:absoluteTagEnd intoString:&chunkString];
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];
1320 //Ignore <meta> tags
1321 } else if ([chunkString caseInsensitiveCompare:@"META"] == NSOrderedSame ||
1322 ([chunkString caseInsensitiveCompare:@"/META"] == NSOrderedSame)) {
1323 [scanner scanUpToCharactersFromSet:absoluteTagEnd intoString:&chunkString];
1325 //Ignore <ul>, </ul>, and </li>
1326 } else if (([chunkString caseInsensitiveCompare:@"UL"] == NSOrderedSame) ||
1327 ([chunkString caseInsensitiveCompare:@"/UL"] == NSOrderedSame) ||
1328 ([chunkString caseInsensitiveCompare:@"/LI"] == NSOrderedSame)) {
1330 //Convert <li> into a bullet point
1331 } else if ([chunkString caseInsensitiveCompare:@"LI"] == NSOrderedSame) {
1332 [attrString appendString:@"• " withAttributes:[textAttributes dictionary]];
1340 //Skip over the end tag character '>' and any other characters we want to skip
1342 //Get to the > if we're not there already, as will happen with XML namespacing...
1343 [scanner scanUpToCharactersFromSet:absoluteTagEnd intoString:NULL];
1346 if (![scanner isAtEnd]) {
1347 [scanner setScanLocation:[scanner scanLocation]+1];
1349 //Skip any other characters we are supposed to skip before continuing
1350 if (charactersToSkipAfterThisTag) {
1351 NSCharacterSet *charSetToSkip;
1353 charSetToSkip = [NSCharacterSet characterSetWithCharactersInString:charactersToSkipAfterThisTag];
1354 [scanner scanCharactersFromSet:charSetToSkip
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];
1365 } else if ([tagOpen compare:@"&"] == NSOrderedSame) { // escape character, eg >
1366 BOOL validTag = [scanner scanUpToCharactersFromSet:charEnd intoString:&chunkString];
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]];
1374 } else if ([chunkString caseInsensitiveCompare:@"LT"] == NSOrderedSame) {
1375 [attrString appendString:@"<" withAttributes:[textAttributes dictionary]];
1377 } else if ([chunkString caseInsensitiveCompare:@"AMP"] == NSOrderedSame) {
1378 [attrString appendString:@"&" withAttributes:[textAttributes dictionary]];
1380 } else if ([chunkString caseInsensitiveCompare:@"QUOT"] == NSOrderedSame) {
1381 [attrString appendString:@"\"" withAttributes:[textAttributes dictionary]];
1383 } else if ([chunkString caseInsensitiveCompare:@"APOS"] == NSOrderedSame) {
1384 [attrString appendString:@"'" withAttributes:[textAttributes dictionary]];
1386 } else if ([chunkString caseInsensitiveCompare:@"NBSP"] == NSOrderedSame) {
1387 [attrString appendString:@" " withAttributes:[textAttributes dictionary]];
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];
1400 if([decScanner scanInt:&character])
1401 [attrString appendString:[NSString stringWithFormat:@"%C", character]
1402 withAttributes:[textAttributes dictionary]];
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];
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];
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)];
1423 [attrString appendString:tagOpen withAttributes:[textAttributes dictionary]];
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).
1434 if ([attrString length]) {
1436 NSColor *bodyColor = [attrString attribute:NSBackgroundColorAttributeName
1438 effectiveRange:&backRange];
1439 if (bodyColor && (backRange.length == [attrString length])) {
1440 [attrString addAttribute:AIBodyColorAttributeName
1442 range:NSMakeRange(0,[attrString length])];
1443 [attrString removeAttribute:NSBackgroundColorAttributeName
1444 range:NSMakeRange(0,[attrString length])];
1448 [myBaseURL release];
1450 return [attrString autorelease];
1453 #pragma mark Tag-parsing
1455 /*methods in this section take a parsed tag (see -parseArguments:) and transfer
1456 * its specification to a text-attributes object.
1459 - (void)restoreAttributesFromDict:(NSDictionary *)inAttributes intoAttributes:(AITextAttributes *)textAttributes
1461 for (NSString *key in inAttributes) {
1462 id value = [inAttributes objectForKey:key];
1463 SEL selector = NSSelectorFromString(key);
1464 if (value == [NSNull null]) value = nil;
1466 [textAttributes performSelector:selector
1471 //Process the contents of a font tag
1472 - (NSDictionary *)processFontTagArgs:(NSDictionary *)inArgs attributes:(AITextAttributes *)textAttributes
1474 NSMutableDictionary *originalAttributes = [NSMutableDictionary dictionary];
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:"];
1481 [textAttributes setFontFamily:[inArgs objectForKey:arg]];
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);
1490 [originalAttributes setObject:[NSNumber numberWithInt:[textAttributes fontSize]]
1491 forKey:@"setFontSizeFromNumber:"];
1493 [textAttributes setFontSize:size];
1496 } else if ([arg caseInsensitiveCompare:@"absz"] == NSOrderedSame) {
1497 [originalAttributes setObject:[NSNumber numberWithInt:[textAttributes fontSize]]
1498 forKey:@"setFontSizeFromNumber:"];
1500 [textAttributes setFontSize:[[inArgs objectForKey:arg] intValue]];
1502 } else if ([arg caseInsensitiveCompare:@"color"] == NSOrderedSame) {
1503 [originalAttributes setObject:([textAttributes textColor] ? (id)[textAttributes textColor] : (id)[NSNull null])
1504 forKey:@"setTextColor:"];
1506 [textAttributes setTextColor:[NSColor colorWithHTMLString:[inArgs objectForKey:arg]
1507 defaultColor:[NSColor blackColor]]];
1509 } else if ([arg caseInsensitiveCompare:@"back"] == NSOrderedSame) {
1510 [originalAttributes setObject:([textAttributes textBackgroundColor] ? (id)[textAttributes textBackgroundColor] : (id)[NSNull null])
1511 forKey:@"setTextBackgroundColor:"];
1513 [textAttributes setTextBackgroundColor:[NSColor colorWithHTMLString:[inArgs objectForKey:arg]
1514 defaultColor:[NSColor whiteColor]]];
1516 } else if ([arg caseInsensitiveCompare:@"lang"] == NSOrderedSame) {
1517 [originalAttributes setObject:([textAttributes languageValue] ? (id)[textAttributes languageValue] : (id)[NSNull null])
1518 forKey:@"setLanguageValue:"];
1520 [textAttributes setLanguageValue:[inArgs objectForKey:arg]];
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])
1527 forKey:@"setTextColor:"];
1528 [textAttributes setTextColor:[NSColor colorWithCalibratedRed:0.0 green:0.5 blue:0.0 alpha:1.0]];
1530 } else if (inDiv && receive) {
1531 [originalAttributes setObject:([textAttributes textColor] ? (id)[textAttributes textColor] : (id)[NSNull null])
1532 forKey:@"setTextColor:"];
1534 [textAttributes setTextColor:[NSColor colorWithCalibratedRed:0.0 green:0.0 blue:0.5 alpha:1.0]];
1539 return originalAttributes;
1542 - (void)processBodyTagArgs:(NSDictionary *)inArgs attributes:(AITextAttributes *)textAttributes
1544 for (NSString *arg in inArgs) {
1545 if ([arg caseInsensitiveCompare:@"bgcolor"] == NSOrderedSame) {
1546 [textAttributes setBackgroundColor:[NSColor colorWithHTMLString:[inArgs objectForKey:arg] defaultColor:[NSColor whiteColor]]];
1551 - (NSDictionary *)processSpanTagArgs:(NSDictionary *)inArgs attributes:(AITextAttributes *)textAttributes
1553 NSMutableDictionary *originalAttributes = [NSMutableDictionary dictionary];
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];
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
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
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]];
1582 } else if ([arg caseInsensitiveCompare:@"style"] == NSOrderedSame) {
1583 NSString *styleList = [inArgs objectForKey:arg];
1584 NSRange attributeRange;
1586 NSScanner *styleScanner = [NSScanner scannerWithString:styleList];
1589 while([styleScanner scanUpToString:@";" intoString:&style])
1591 [styleScanner scanString:@";" intoString:nil];
1593 int styleLength = [style length];
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]];
1600 [originalAttributes setObject:([textAttributes fontFamily] ? (id)[textAttributes fontFamily] : (id)[NSNull null])
1601 forKey:@"setFontFamily:"];
1603 [textAttributes setFontFamily:fontFamily];
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]];
1611 static int stylePointSizes[] = { 9, 10, 12, 14, 18, 24 };
1614 if ([fontSize caseInsensitiveCompare:@"xx-small"] == NSOrderedSame) {
1615 size = stylePointSizes[0];
1617 } else if ([fontSize caseInsensitiveCompare:@"x-small"] == NSOrderedSame) {
1618 size = stylePointSizes[1];
1620 } else if ([fontSize caseInsensitiveCompare:@"small"] == NSOrderedSame) {
1621 size = stylePointSizes[2];
1623 } else if ([fontSize caseInsensitiveCompare:@"medium"] == NSOrderedSame) {
1624 size = stylePointSizes[3];
1626 } else if ([fontSize caseInsensitiveCompare:@"large"] == NSOrderedSame) {
1627 size = stylePointSizes[4];
1629 } else if ([fontSize caseInsensitiveCompare:@"x-large"] == NSOrderedSame) {
1630 size = stylePointSizes[5];
1632 NSRange pixelUnits = [fontSize rangeOfString:@"px"
1633 options:NSLiteralSearch];
1634 if (pixelUnits.location != NSNotFound) {
1635 size = [[fontSize substringWithRange:NSMakeRange(0,pixelUnits.location)] intValue];
1639 [originalAttributes setObject:[NSNumber numberWithInt:[textAttributes fontSize]]
1640 forKey:@"setFontSizeFromNumber:"];
1641 [textAttributes setFontSize:size];
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]];
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];
1656 [textAttributes disableTrait:NSBoldFontMask];
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]];
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];
1671 [textAttributes disableTrait:NSItalicFontMask];
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]];
1680 NSScanner *fontStringScanner = [NSScanner scannerWithString:fontString];
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];
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]];
1699 [originalAttributes setObject:([textAttributes backgroundColor] ? (id)[textAttributes backgroundColor] : (id)[NSNull null])
1700 forKey:@"setBackgroundColor:"];
1701 [textAttributes setBackgroundColor:[NSColor colorWithHTMLString:hexColor
1702 defaultColor:[NSColor blackColor]]];
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: ')
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]];
1716 [originalAttributes setObject:([textAttributes textColor] ? (id)[textAttributes textColor] : (id)[NSNull null])
1717 forKey:@"setTextColor:"];
1718 [textAttributes setTextColor:[NSColor colorWithHTMLString:hexColor
1719 defaultColor:[NSColor blackColor]]];
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]];
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];
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){
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){
1749 return originalAttributes;
1752 - (void)processLinkTagArgs:(NSDictionary *)inArgs attributes:(AITextAttributes *)textAttributes
1754 for (NSString *arg in inArgs) {
1755 if ([arg caseInsensitiveCompare:@"href"] == NSOrderedSame) {
1756 NSString *linkString = [inArgs objectForKey:arg];
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.
1763 if ([linkString rangeOfString:@"%n"].location != NSNotFound) {
1764 NSMutableString *newLinkString = [[linkString mutableCopy] autorelease];
1765 [newLinkString replaceOccurrencesOfString:@"%n"
1767 options:NSLiteralSearch
1768 range:NSMakeRange(0, [newLinkString length])];
1769 linkString = newLinkString;
1772 //NSURL does not expect an HTML-escaped string, but HTML-escape codes are valid within links (e.g. &)
1773 linkString = [linkString stringByUnescapingFromXMLWithEntities:nil];
1776 [textAttributes setLinkURL:[NSURL URLWithString:linkString relativeToURL:[NSURL URLWithString:baseURL]]];
1778 [textAttributes setLinkURL:[NSURL URLWithString:linkString]];
1783 - (void)processDivTagArgs:(NSDictionary *)inArgs attributes:(AITextAttributes *)textAttributes
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];
1790 if ([direction caseInsensitiveCompare:@"rtl"] == NSOrderedSame) {
1791 [textAttributes setWritingDirection:NSWritingDirectionRightToLeft];
1793 } else if ([direction caseInsensitiveCompare:@"ltr"] == NSOrderedSame) {
1794 [textAttributes setWritingDirection:NSWritingDirectionLeftToRight];
1797 } else if ([arg caseInsensitiveCompare:@"class"] == NSOrderedSame) {
1798 NSString *class = [inArgs objectForKey:arg];
1799 if ([class caseInsensitiveCompare:@"send"] == NSOrderedSame) {
1802 } else if ([class caseInsensitiveCompare:@"receive"] == NSOrderedSame) {
1805 } else if ([class caseInsensitiveCompare:@"status"] == NSOrderedSame) {
1806 [textAttributes setTextColor:[NSColor grayColor]];
1812 - (NSAttributedString *)processImgTagArgs:(NSDictionary *)inArgs attributes:(AITextAttributes *)textAttributes baseURL:(NSString *)inBaseURL
1814 NSAttributedString *attachString;
1815 AITextAttachmentExtension *attachment = [[[AITextAttachmentExtension alloc] init] autorelease];
1817 for (NSString *arg in inArgs) {
1818 if ([arg caseInsensitiveCompare:@"src"] == NSOrderedSame) {
1819 NSString *src = [inArgs objectForKey:arg];
1822 if ([src rangeOfString:@"://"].location != NSNotFound) {
1824 [NSURL URLWithString:src relativeToURL:[NSURL URLWithString:baseURL]] :
1825 [NSURL URLWithString:src]);
1827 url = [NSURL fileURLWithPath:(baseURL ?
1828 [baseURL stringByAppendingPathComponent:src] :
1832 if (url && [url isFileURL]) {
1835 if (inBaseURL && ![[NSFileManager defaultManager] fileExistsAtPath:src])
1836 src = [inBaseURL stringByAppendingPathComponent:src];
1838 return [NSAttributedString attributedStringWithLinkLabel:src linkDestination:src];
1841 [attachment setPath:src];
1843 if ([arg caseInsensitiveCompare:@"alt"] == NSOrderedSame) {
1844 [attachment setString:[inArgs objectForKey:arg]];
1845 [attachment setHasAlternate:YES];
1847 if ([arg caseInsensitiveCompare:@"class"] == NSOrderedSame) {
1848 [attachment setImageClass:[inArgs objectForKey:arg]];
1852 [attachment setShouldSaveImageForLogging:YES];
1854 //Use the real image if possible
1855 NSImage *image = [attachment image];
1857 //Otherwise, use an icon representing the image
1858 if (!image) image = [attachment iconImage];
1861 NSTextAttachmentCell *cell = [[NSTextAttachmentCell alloc] initImageCell:image];
1862 [attachment setAttachmentCell:cell];
1865 attachString = [NSAttributedString attributedStringWithAttachment:attachment];
1870 return attachString;
1874 * @brief Append an image to the HTML
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.
1884 * @result YES if successful.
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
1894 NSString *shortFileName;
1897 if (imagesPath || !inPath) {
1898 //create the images directory if it doesn't exist
1900 [[NSFileManager defaultManager] createDirectoryAtPath:imagesPath withIntermediateDirectories:YES attributes:nil error:NULL];
1904 //Just get it from the original path. This is especially good for emoticons.
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.
1912 imagesPath = NSTemporaryDirectory();
1915 //Make sure the image has an appropriate extension
1916 if ([[inName pathExtension] caseInsensitiveCompare:@"png"] != NSOrderedSame) {
1917 inName = [inName stringByAppendingPathExtension:@"png"];
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];
1928 NSLog(@"Failed to write image %@",inName);
1936 NSString *srcPath = [[[NSURL fileURLWithPath:inPath] absoluteString] stringByEscapingForXMLWithEntities:nil];
1937 NSString *altName = (inName ? [inName stringByEscapingForXMLWithEntities:nil] : [srcPath lastPathComponent]);
1939 //Note the space at the end of the tag
1940 NSString *imageClassTag = (imageClass ? [NSString stringWithFormat:@"class=\"%@\" ", imageClass] : @"");
1942 if (attachmentImage) {
1943 //Include size information if possible
1944 NSSize imageSize = [attachmentImage size];
1945 [string appendFormat:@"<img %@src=\"%@%@\" alt=\"%@\" width=\"%i\" height=\"%i\">",
1947 srcPath, (uniqueifyHTML ? [NSString stringWithFormat:@"?%i", [[NSDate date] timeIntervalSince1970]] : @""),
1949 (int)imageSize.width, (int)imageSize.height];
1952 [string appendFormat:@"<img %@src=\"%@%@\" alt=\"%@\">",
1954 srcPath, (uniqueifyHTML ? [NSString stringWithFormat:@"?%i", [[NSDate date] timeIntervalSince1970]] : @""),
1962 #pragma mark Properties
1964 @synthesize XMLNamespace;
1966 - (BOOL)generatesStrictXHTML
1968 return thingsToInclude.generateStrictXHTML;
1970 - (void)setGeneratesStrictXHTML:(BOOL)newValue
1972 thingsToInclude.generateStrictXHTML = newValue;
1975 - (BOOL)includesHeaders
1977 return thingsToInclude.headers;
1979 - (void)setIncludesHeaders:(BOOL)newValue
1981 thingsToInclude.headers = newValue;
1984 - (BOOL)includesFontTags
1986 return thingsToInclude.fontTags;
1988 - (void)setIncludesFontTags:(BOOL)newValue
1990 thingsToInclude.fontTags = newValue;
1993 - (BOOL)closesFontTags
1995 return thingsToInclude.closingFontTags;
1997 - (void)setClosesFontTags:(BOOL)newValue
1999 thingsToInclude.closingFontTags = newValue;
2002 - (BOOL)includesColorTags
2004 return thingsToInclude.colorTags;
2006 - (void)setIncludesColorTags:(BOOL)newValue
2008 thingsToInclude.colorTags = newValue;
2011 - (BOOL)includesStyleTags
2013 return thingsToInclude.styleTags;
2015 - (void)setIncludesStyleTags:(BOOL)newValue
2017 thingsToInclude.styleTags = newValue;
2020 - (BOOL)encodesNonASCII
2022 return thingsToInclude.nonASCII;
2024 - (void)setEncodesNonASCII:(BOOL)newValue
2026 thingsToInclude.nonASCII = newValue;
2029 - (BOOL)preservesAllSpaces
2031 return thingsToInclude.allSpaces;
2033 - (void)setPreservesAllSpaces:(BOOL)newValue
2035 thingsToInclude.allSpaces = newValue;
2038 - (BOOL)usesAttachmentTextEquivalents
2040 return thingsToInclude.attachmentTextEquivalents;
2042 - (void)setUsesAttachmentTextEquivalents:(BOOL)newValue
2044 thingsToInclude.attachmentTextEquivalents = newValue;
2047 - (BOOL)onlyConvertImageAttachmentsToIMGTagsWhenSendingAMessage
2049 return thingsToInclude.onlyIncludeOutgoingImages;
2051 - (void)setOnlyConvertImageAttachmentsToIMGTagsWhenSendingAMessage:(BOOL)newValue
2053 thingsToInclude.onlyIncludeOutgoingImages = newValue;
2056 - (BOOL)onlyUsesSimpleTags
2058 return thingsToInclude.simpleTagsOnly;
2060 - (void)setOnlyUsesSimpleTags:(BOOL)newValue
2062 thingsToInclude.simpleTagsOnly = newValue;
2065 - (BOOL)includesBodyBackground
2067 return thingsToInclude.bodyBackground;
2069 - (void)setIncludesBodyBackground:(BOOL)newValue
2071 thingsToInclude.bodyBackground = newValue;
2074 - (BOOL)allowAIMsubprofileLinks
2076 return thingsToInclude.allowAIMsubprofileLinks;
2078 - (void)setAllowAIMsubprofileLinks:(BOOL)newValue
2080 thingsToInclude.allowAIMsubprofileLinks = newValue;
2083 - (BOOL)allowJavascriptURLs
2085 return thingsToInclude.allowJavascriptURLs;
2087 - (void)setAllowJavascriptURLs:(BOOL)newValue
2089 thingsToInclude.allowJavascriptURLs = newValue;
2092 @synthesize baseURL;
2096 static AIHTMLDecoder *classMethodInstance = nil;
2098 @implementation AIHTMLDecoder (ClassMethodCompatibility)
2100 + (AIHTMLDecoder *)classMethodInstance
2102 if (classMethodInstance == nil)
2103 classMethodInstance = [[self alloc] init];
2104 return classMethodInstance;
2108 + (NSString *)encodeHTML:(NSAttributedString *)inMessage encodeFullString:(BOOL)encodeFullString
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 =
2118 classMethodInstance->thingsToInclude.styleTags =
2119 classMethodInstance->thingsToInclude.attachmentTextEquivalents =
2121 classMethodInstance->thingsToInclude.onlyIncludeOutgoingImages =
2122 classMethodInstance->thingsToInclude.simpleTagsOnly =
2123 classMethodInstance->thingsToInclude.bodyBackground =
2124 classMethodInstance->thingsToInclude.allowAIMsubprofileLinks =
2127 return [classMethodInstance encodeHTML:inMessage imagesPath:nil];
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   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
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;
2175 return [classMethodInstance encodeHTML:inMessage imagesPath:imagesPath];
2178 + (NSAttributedString *)decodeHTML:(NSString *)inMessage
2180 return [[self classMethodInstance] decodeHTML:inMessage withDefaultAttributes:nil];
2183 + (NSAttributedString *)decodeHTML:(NSString *)inMessage withDefaultAttributes:(NSDictionary *)inDefaultAttributes
2185 return [[self classMethodInstance] decodeHTML:inMessage withDefaultAttributes:inDefaultAttributes];
2188 + (NSDictionary *)parseArguments:(NSString *)arguments
2190 return [[self classMethodInstance] parseArguments:arguments];
2195 #pragma mark C functions
2197 int HTMLEquivalentForFontSize(int fontSize)
2199 if (fontSize <= 9) {
2201 } else if (fontSize <= 10) {
2203 } else if (fontSize <= 12) {
2205 } else if (fontSize <= 14) {
2207 } else if (fontSize <= 18) {
2209 } else if (fontSize <= 24) {
2216 @implementation NSString (AIHTMLDecoderAdditions)
2219 * @brief Allow absoluteString to be called on NSString objects
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.
2226 - (NSString *)absoluteString
2232 * @brief Convert ASCII Symbol font to the appropriate Unicode characters
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).
2238 * Characters which can't be converted are replaced by ''.
2240 - (NSString *)stringByConvertingSymbolToSymbolUnicode
2242 NSMutableString *decodedString = [NSMutableString string];
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" };
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"};
2272 NSData *utf8Data = [self dataUsingEncoding:NSUTF8StringEncoding];
2273 const char *utf8String = [utf8Data bytes];
2274 unsigned sourceLength = [utf8Data length];
2276 for (int i = 0; i < sourceLength; i++) {
2277 unichar ch = utf8String[i];
2278 const char *replacement;
2280 if (ch >= 32 && ch <= 126) {
2281 replacement = lowSymbolTable[ch - 32];
2283 } else if (ch >= 161 && ch <= 254) {
2284 replacement = highSymbolTable[ch - 161];
2290 if (replacement && strlen(replacement)) {
2291 [decodedString appendString:[NSString stringWithUTF8String:replacement]];
2294 [decodedString appendFormat:@"%c", ch];
2298 return decodedString;
2302 * @brief Convert Wingdings characters to their Unicode equivalents if possible
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 ''.
2307 - (NSString *)stringByConvertingWingdingsToUnicode
2309 NSMutableString *decodedString = [NSMutableString string];
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", ""};
2340 NSData *utf8Data = [self dataUsingEncoding:NSUTF8StringEncoding];
2341 const char *utf8String = [utf8Data bytes];
2342 unsigned sourceLength = [utf8Data length];
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]]];
2349 [decodedString appendFormat:@"%c", ch];
2353 return decodedString;