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 <AIUtilities/NSCalendarDate+ISO8601Parsing.h>
24 #import <AIUtilities/AIDateFormatterAdditions.h>
25 #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(@"Offline", nil), @"offline",
66 AILocalizedString(@"Away", nil), @"away",
67 AILocalizedString(@"Idle", nil), @"idle",
68 AILocalizedString(@"Available", nil), @"available",
69 AILocalizedString(@"Busy", nil), @"busy",
70 AILocalizedString(@"Not at Home", nil), @"notAtHome",
71 AILocalizedString(@"On the Phone", nil), @"onThePhone",
72 AILocalizedString(@"On Vacation", nil), @"onVacation",
73 AILocalizedString(@"Do Not Disturb", nil), @"doNotDisturb",
74 AILocalizedString(@"Extended Away", nil), @"extendedAway",
75 AILocalizedString(@"Be Right Back", nil), @"beRightBack",
76 AILocalizedString(@"Not Available", nil), @"notAvailable",
77 AILocalizedString(@"Not at my Desk", nil), @"notAtMyDesk",
78 AILocalizedString(@"Not in the Office", nil), @"notInTheOffice",
79 AILocalizedString(@"Stepped Out", nil), @"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 message = [inputFileString substringWithRange:NSMakeRange(messageStart, end - messageStart - 11)]; // 10 for </message> and 1 for the index being off
240 NSString *shownSender;
242 NSString *displayName = nil, *longDisplayName = nil;
244 if ([mySN isEqualToString:sender]) {
245 //Find an account if one exists, and use its name
246 displayName = (myDisplayName ? myDisplayName : sender);
249 AIListObject *listObject = [[adium contactController] existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:service UID:sender]];
251 cssClass = @"receive";
252 displayName = [listObject displayName];
253 longDisplayName = [listObject longDisplayName];
256 if (displayName && ![displayName isEqualToString:sender]) {
257 switch (nameFormat) {
259 shownSender = (longDisplayName ? longDisplayName : displayName);
263 shownSender = displayName;
266 case AIDisplayName_ScreenName:
267 shownSender = [NSString stringWithFormat:@"%@ (%@)",displayName,sender];
270 case AIScreenName_DisplayName:
271 shownSender = [NSString stringWithFormat:@"%@ (%@)",sender,displayName];
275 shownSender = sender;
279 shownSender = sender;
282 [output appendFormat:@"<div class=\"%@\"><span class=\"timestamp\">%@</span> <span class=\"sender\">%@%@: </span><pre class=\"message\">%@</pre></div>\n",
283 ([mySN isEqualToString:sender] ? @"send" : @"receive"),
284 [date descriptionWithCalendarFormat:[NSDateFormatter localizedDateFormatStringShowingSeconds:YES
289 (autoResponse ? AILocalizedString(@" (Autoreply)",nil) : @""),
291 state = XML_STATE_CHAT;
294 case XML_STATE_STATUS_MESSAGE:
295 if([name isEqualToString:@"status"])
297 CFIndex end = CFXMLParserGetLocation(parser);
298 NSString *message = nil;
300 message = [inputFileString substringWithRange:NSMakeRange(messageStart, end - messageStart - 10)]; // 9 for </status> and 1 for the index being off
302 NSString *displayMessage = nil;
303 //Note: I am diverging from what the AILoggerPlugin logs in this case. It can't handle every case we can have here
307 displayMessage = [NSString stringWithFormat:AILocalizedString(@"Changed status to %@: %@", nil), [statusLookup objectForKey:status], message];
309 displayMessage = [NSString stringWithFormat:AILocalizedString(@"Changed status to %@", nil), message];
311 else if([status length])
312 displayMessage = [NSString stringWithFormat:AILocalizedString(@"Changed status to %@", nil), [statusLookup objectForKey:status]];
314 if([displayMessage length])
315 [output appendFormat:@"<div class=\"status\">%@ (%@)</div>\n",
317 [date descriptionWithCalendarFormat:[NSDateFormatter localizedDateFormatStringShowingSeconds:YES
321 state = XML_STATE_CHAT;
324 if([name isEqualToString:@"chat"])
325 state = XML_STATE_NONE;
337 void *createStructure(CFXMLParserRef parser, CFXMLNodeRef node, void *context)
341 // Use the dataTypeID to determine what to print.
342 switch (CFXMLNodeGetTypeCode(node)) {
343 case kCFXMLNodeTypeDocument:
345 case kCFXMLNodeTypeElement:
347 NSString *name = [NSString stringWithString:(NSString *)CFXMLNodeGetString(node)];
348 const CFXMLElementInfo *info = CFXMLNodeGetInfoPtr(node);
349 [(GBChatlogHTMLConverter *)context startedElement:name info:info];
350 ret = (element *)malloc(sizeof(element));
351 ret->name = [name retain];
352 ret->empty = info->isEmpty;
355 case kCFXMLNodeTypeProcessingInstruction:
356 case kCFXMLNodeTypeComment:
357 case kCFXMLNodeTypeText:
358 case kCFXMLNodeTypeCDATASection:
359 case kCFXMLNodeTypeEntityReference:
360 case kCFXMLNodeTypeDocumentType:
361 case kCFXMLNodeTypeWhitespace:
366 // Return the data string for use by the addChild and
367 // endStructure callbacks.
371 void addChild(CFXMLParserRef parser, void *parent, void *child, void *context)
375 void endStructure(CFXMLParserRef parser, void *xmlType, void *context)
377 NSString *name = nil;
381 name = [NSString stringWithString:((element *)xmlType)->name];
382 empty = ((element *)xmlType)->empty;
384 [(GBChatlogHTMLConverter *)context endedElement:name empty:empty];
387 [((element *)xmlType)->name release];