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