2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "GBChatlogHTMLConverter.h"
18 #import "AIStandardListWindowController.h"
19 #import <Adium/AIListContact.h>
20 #import <Adium/AIAccountControllerProtocol.h>
21 #import <Adium/AIContactControllerProtocol.h>
22 #import <Adium/AIPreferenceControllerProtocol.h>
23 #import <Adium/AIStatusControllerProtocol.h>
24 #import <AIUtilities/NSCalendarDate+ISO8601Parsing.h>
25 #import <AIUtilities/AIDateFormatterAdditions.h>
26 #import <AIUtilities/AIStringAdditions.h>
29 #define PREF_GROUP_WEBKIT_MESSAGE_DISPLAY @"WebKit Message Display"
30 #define KEY_WEBKIT_USE_NAME_FORMAT @"Use Custom Name Format"
31 #define KEY_WEBKIT_NAME_FORMAT @"Name Format"
33 static void *createStructure(CFXMLParserRef parser, CFXMLNodeRef node, void *context);
34 static void addChild(CFXMLParserRef parser, void *parent, void *child, void *context);
35 static void endStructure(CFXMLParserRef parser, void *xmlType, void *context);
37 @implementation GBChatlogHTMLConverter
39 + (NSString *)readFile:(NSString *)filePath
41 GBChatlogHTMLConverter *converter = [[GBChatlogHTMLConverter alloc] init];
42 NSString *ret = [[converter readFile:filePath] retain];
44 return [ret autorelease];
53 state = XML_STATE_NONE;
55 inputFileString = nil;
63 statusLookup = [[NSDictionary alloc] initWithObjectsAndKeys:
64 AILocalizedString(@"Online", nil), @"online",
65 AILocalizedString(@"Idle", nil), @"idle",
66 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_OFFLINE], @"offline",
67 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_AWAY], @"away",
68 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_AVAILABLE], @"available",
69 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_BUSY], @"busy",
70 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_NOT_AT_HOME], @"notAtHome",
71 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_PHONE], @"onThePhone",
72 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_VACATION], @"onVacation",
73 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_DND], @"doNotDisturb",
74 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_EXTENDED_AWAY], @"extendedAway",
75 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_BRB], @"beRightBack",
76 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_NOT_AVAILABLE], @"notAvailable",
77 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_NOT_AT_DESK], @"notAtMyDesk",
78 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_NOT_IN_OFFICE], @"notInTheOffice",
79 [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_STEPPED_OUT], @"steppedOut",
82 if ([[[adium preferenceController] preferenceForKey:KEY_WEBKIT_USE_NAME_FORMAT
83 group:PREF_GROUP_WEBKIT_MESSAGE_DISPLAY] boolValue]) {
84 nameFormat = [[[adium preferenceController] preferenceForKey:KEY_WEBKIT_NAME_FORMAT
85 group:PREF_GROUP_WEBKIT_MESSAGE_DISPLAY] intValue];
87 nameFormat = AIDefaultName;
95 [inputFileString release];
96 [eventTranslate release];
99 [myDisplayName release];
104 [statusLookup release];
108 - (NSString *)readFile:(NSString *)filePath
110 NSData *inputData = [NSData dataWithContentsOfFile:filePath];
111 inputFileString = [[NSString alloc] initWithData:inputData encoding:NSUTF8StringEncoding];
112 NSURL *url = [[NSURL alloc] initFileURLWithPath:filePath];
113 output = [[NSMutableString alloc] init];
115 CFXMLParserCallBacks callbacks = {
123 CFXMLParserContext context = {
130 parser = CFXMLParserCreateWithDataFromURL(NULL, (CFURLRef)url, kCFXMLParserSkipMetaData | kCFXMLParserSkipWhitespace, kCFXMLNodeCurrentVersion, &callbacks, &context);
131 if (!CFXMLParserParse(parser)) {
132 NSLog(@"%@: Parser %@ for inputFileString %@ returned false.",
133 [self class], parser, inputFileString);
143 - (void)startedElement:(NSString *)name info:(const CFXMLElementInfo *)info
145 NSDictionary *attributes = (NSDictionary *)info->attributes;
149 if([name isEqualToString:@"chat"])
152 mySN = [[attributes objectForKey:@"account"] retain];
155 service = [[attributes objectForKey:@"service"] retain];
157 [myDisplayName release];
160 NSEnumerator *enumerator = [[[adium accountController] accounts] objectEnumerator];
163 while ((account = [enumerator nextObject])) {
164 if ([[[account UID] compactedString] isEqualToString:[mySN compactedString]] &&
165 [[account serviceID] isEqualToString:service]) {
166 myDisplayName = [[account displayName] retain];
170 state = XML_STATE_CHAT;
174 if([name isEqualToString:@"message"])
179 NSString *dateStr = [attributes objectForKey:@"time"];
181 date = [[NSCalendarDate calendarDateWithString:dateStr] retain];
184 sender = [[attributes objectForKey:@"sender"] retain];
185 autoResponse = [[attributes objectForKey:@"auto"] isEqualToString:@"true"];
187 //Mark the location of the message... We can copy it directly. Anyone know why it is off by 1?
188 messageStart = CFXMLParserGetLocation(parser) - 1;
190 state = XML_STATE_MESSAGE;
192 else if([name isEqualToString:@"event"])
194 //Mark the location of the message... We can copy it directly. Anyone know why it is off by 1?
195 messageStart = CFXMLParserGetLocation(parser) - 1;
197 state = XML_STATE_EVENT_MESSAGE;
199 else if([name isEqualToString:@"status"])
204 NSString *dateStr = [attributes objectForKey:@"time"];
206 date = [[NSCalendarDate calendarDateWithString:dateStr] retain];
210 status = [[attributes objectForKey:@"type"] retain];
212 //Mark the location of the message... We can copy it directly. Anyone know why it is off by 1?
213 messageStart = CFXMLParserGetLocation(parser) - 1;
215 state = XML_STATE_STATUS_MESSAGE;
218 case XML_STATE_MESSAGE:
219 case XML_STATE_EVENT_MESSAGE:
220 case XML_STATE_STATUS_MESSAGE:
225 - (void)endedElement:(NSString *)name empty:(BOOL)empty
229 case XML_STATE_EVENT_MESSAGE:
230 state = XML_STATE_CHAT;
233 case XML_STATE_MESSAGE:
234 if([name isEqualToString:@"message"])
236 CFIndex end = CFXMLParserGetLocation(parser);
237 NSString *message = nil;
239 // Need to unescape entities ourself. & wasn't getting unescaped for some reason. See #6850.
240 // 10 for </message> and 1 for the index being off
241 message = [[inputFileString substringWithRange:NSMakeRange(messageStart, end - messageStart - 11)] stringByUnescapingFromXMLWithEntities:nil];
243 NSString *shownSender = sender;
245 NSString *displayName = nil, *longDisplayName = nil;
247 if ([mySN isEqualToString:sender]) {
248 //Find an account if one exists, and use its name
249 displayName = (myDisplayName ? myDisplayName : sender);
252 AIListObject *listObject = [[adium contactController] existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:service UID:sender]];
254 cssClass = @"receive";
255 displayName = [listObject displayName];
256 longDisplayName = [listObject longDisplayName];
259 if (displayName && ![displayName isEqualToString:sender]) {
260 switch (nameFormat) {
262 shownSender = (longDisplayName ? longDisplayName : displayName);
266 shownSender = displayName;
269 case AIDisplayName_ScreenName:
270 shownSender = [NSString stringWithFormat:@"%@ (%@)",displayName,sender];
273 case AIScreenName_DisplayName:
274 shownSender = [NSString stringWithFormat:@"%@ (%@)",sender,displayName];
278 shownSender = sender;
283 [output appendFormat:@"<div class=\"%@\"><span class=\"timestamp\">%@</span> <span class=\"sender\">%@%@: </span><pre class=\"message\">%@</pre></div>\n",
284 ([mySN isEqualToString:sender] ? @"send" : @"receive"),
285 [date descriptionWithCalendarFormat:[NSDateFormatter localizedDateFormatStringShowingSeconds:YES
290 (autoResponse ? AILocalizedString(@" (Autoreply)",nil) : @""),
292 state = XML_STATE_CHAT;
295 case XML_STATE_STATUS_MESSAGE:
296 if([name isEqualToString:@"status"])
298 CFIndex end = CFXMLParserGetLocation(parser);
299 NSString *message = nil;
301 message = [inputFileString substringWithRange:NSMakeRange(messageStart, end - messageStart - 10)]; // 9 for </status> and 1 for the index being off
303 NSString *displayMessage = nil;
304 //Note: I am diverging from what the AILoggerPlugin logs in this case. It can't handle every case we can have here
308 displayMessage = [NSString stringWithFormat:AILocalizedString(@"Changed status to %@: %@", nil), [statusLookup objectForKey:status], message];
310 displayMessage = [NSString stringWithFormat:AILocalizedString(@"Changed status to %@", nil), message];
312 else if([status length])
313 displayMessage = [NSString stringWithFormat:AILocalizedString(@"Changed status to %@", nil), [statusLookup objectForKey:status]];
315 if([displayMessage length])
316 [output appendFormat:@"<div class=\"status\">%@ (%@)</div>\n",
318 [date descriptionWithCalendarFormat:[NSDateFormatter localizedDateFormatStringShowingSeconds:YES
322 state = XML_STATE_CHAT;
325 if([name isEqualToString:@"chat"])
326 state = XML_STATE_NONE;
338 void *createStructure(CFXMLParserRef parser, CFXMLNodeRef node, void *context)
342 // Use the dataTypeID to determine what to print.
343 switch (CFXMLNodeGetTypeCode(node)) {
344 case kCFXMLNodeTypeDocument:
346 case kCFXMLNodeTypeElement:
348 NSString *name = [NSString stringWithString:(NSString *)CFXMLNodeGetString(node)];
349 const CFXMLElementInfo *info = CFXMLNodeGetInfoPtr(node);
350 [(GBChatlogHTMLConverter *)context startedElement:name info:info];
351 ret = (element *)malloc(sizeof(element));
352 ret->name = [name retain];
353 ret->empty = info->isEmpty;
356 case kCFXMLNodeTypeProcessingInstruction:
357 case kCFXMLNodeTypeComment:
358 case kCFXMLNodeTypeText:
359 case kCFXMLNodeTypeCDATASection:
360 case kCFXMLNodeTypeEntityReference:
361 case kCFXMLNodeTypeDocumentType:
362 case kCFXMLNodeTypeWhitespace:
367 // Return the data string for use by the addChild and
368 // endStructure callbacks.
372 void addChild(CFXMLParserRef parser, void *parent, void *child, void *context)
376 void endStructure(CFXMLParserRef parser, void *xmlType, void *context)
378 NSString *name = nil;
382 name = [NSString stringWithString:((element *)xmlType)->name];
383 empty = ((element *)xmlType)->empty;
385 [(GBChatlogHTMLConverter *)context endedElement:name empty:empty];
388 [((element *)xmlType)->name release];