// // $Id$ // // GeneratePreviewForURL.m // sequel-pro // // Created by Hans-Jörg Bibiko on Aug 04, 2010 // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // // More info at <http://code.google.com/p/sequel-pro/> #include <CoreFoundation/CoreFoundation.h> #include <CoreServices/CoreServices.h> #include <QuickLook/QuickLook.h> #import "SPDataAdditions.h" #import "SPStringAdditions.h" #import <Cocoa/Cocoa.h> #import "SPEditorTokens.h" /* ----------------------------------------------------------------------------- Generate a preview for file This function's job is to create preview for designated file ----------------------------------------------------------------------------- */ #pragma mark lex init /* * Include all the extern variables and prototypes required for flex (used for syntax highlighting) */ extern NSUInteger yylex(); extern NSUInteger yyuoffset, yyuleng; typedef struct yy_buffer_state *YY_BUFFER_STATE; void yy_switch_to_buffer(YY_BUFFER_STATE); YY_BUFFER_STATE yy_scan_string (const char *); OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options) { NSURL *myURL = (NSURL *)url; NSString *urlExtension = [[[myURL path] pathExtension] lowercaseString]; NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; NSError *templateReadError = nil; if (QLPreviewRequestIsCancelled(preview)) return noErr; // Get current Sequel Pro's set of file icons NSArray *iconImages = [[[NSWorkspace sharedWorkspace] iconForFile:[myURL path]] representations]; // just in case if(!iconImages || [iconImages count] < 1) iconImages = [NSArray arrayWithObject:[NSImage imageNamed:NSImageNameStopProgressTemplate]]; NSMutableString *html; NSString *template = nil; if (QLPreviewRequestIsCancelled(preview)) return noErr; NSInteger previewHeight = 280; NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[myURL path] error:nil]; // Dispatch different fiel extensions if([urlExtension isEqualToString:@"spf"]) { NSImage *iconImage; if([iconImages count] > 0) iconImage = [iconImages objectAtIndex:1]; else iconImage = [iconImages objectAtIndex:0]; NSError *readError = nil; NSString *convError = nil; NSPropertyListFormat format; NSDictionary *spf = nil; // Get spf data as dictionary NSData *pData = [NSData dataWithContentsOfFile:[myURL path] options:NSUncachedRead error:&readError]; spf = [[NSPropertyListSerialization propertyListFromData:pData mutabilityOption:NSPropertyListImmutable format:&format errorDescription:&convError] retain]; if(!spf || readError != nil || [convError length] || !(format == NSPropertyListXMLFormat_v1_0 || format == NSPropertyListBinaryFormat_v1_0)) { if(spf) [spf release]; [pool release]; return noErr; } // Dispatch different spf formats if([[spf objectForKey:@"format"] isEqualToString:@"connection"]) { template = [NSString stringWithContentsOfFile:[[NSBundle bundleWithIdentifier:@"com.google.code.sequel-pro.qlgenerator"] pathForResource:@"SPQLPluginConnectionTemplate" ofType:@"html"] encoding:NSUTF8StringEncoding error:&templateReadError]; if (template == nil || ![template length] || templateReadError != nil) { [pool release]; return noErr; } NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setTimeStyle:NSDateFormatterShortStyle]; [dateFormatter setDateStyle:NSDateFormatterMediumStyle]; [dateFormatter setLocale:[NSLocale currentLocale]]; NSString *name = @"••••"; NSString *host = @"••••"; NSString *user = @"••••"; NSString *database = @"••••"; NSString *autoConnect = ([[spf objectForKey:@"auto_connect"] boolValue]) ? @"checked" : @""; if([[spf objectForKey:@"data"] isKindOfClass:[NSDictionary class]]) { if([[spf objectForKey:@"data"] objectForKey:@"connection"] && [[[spf objectForKey:@"data"] objectForKey:@"connection"] objectForKey:@"name"]) name = [[[spf objectForKey:@"data"] objectForKey:@"connection"] objectForKey:@"name"]; else name = @""; if([[spf objectForKey:@"data"] objectForKey:@"connection"] && [[[spf objectForKey:@"data"] objectForKey:@"connection"] objectForKey:@"host"]) host = [[[spf objectForKey:@"data"] objectForKey:@"connection"] objectForKey:@"host"]; else host = @""; if([[spf objectForKey:@"data"] objectForKey:@"connection"] && [[[spf objectForKey:@"data"] objectForKey:@"connection"] objectForKey:@"user"]) user = [[[spf objectForKey:@"data"] objectForKey:@"connection"] objectForKey:@"user"]; else user = @""; if([[spf objectForKey:@"data"] objectForKey:@"connection"] && [[[spf objectForKey:@"data"] objectForKey:@"connection"] objectForKey:@"database"]) database = [[[spf objectForKey:@"data"] objectForKey:@"connection"] objectForKey:@"database"]; else database = @""; } // compose the html html = [[NSMutableString alloc] initWithString:[NSString stringWithFormat:template, [[iconImage TIFFRepresentationUsingCompression:NSTIFFCompressionJPEG factor:0.01] base64EncodingWithLineLength:0], [spf objectForKey:@"rdbms_type"], [spf objectForKey:@"rdbms_version"], [name stringByReplacingOccurrencesOfString:@" " withString:@" "], [host stringByReplacingOccurrencesOfString:@" " withString:@" "], [user stringByReplacingOccurrencesOfString:@" " withString:@" "], [database stringByReplacingOccurrencesOfString:@" " withString:@" "], [NSString stringForByteSize:[[fileAttributes objectForKey:NSFileSize] longLongValue]], [dateFormatter stringFromDate:[fileAttributes fileModificationDate]], autoConnect ]]; [dateFormatter release]; } else if([[spf objectForKey:@"format"] isEqualToString:@"content filters"]) { NSImage *iconImage; if([iconImages count] > 0) iconImage = [iconImages objectAtIndex:1]; else iconImage = [iconImages objectAtIndex:0]; template = [NSString stringWithContentsOfFile:[[NSBundle bundleWithIdentifier:@"com.google.code.sequel-pro.qlgenerator"] pathForResource:@"SPQLPluginContentFiltersTemplate" ofType:@"html"] encoding:NSUTF8StringEncoding error:&templateReadError]; if (template == nil || ![template length] || templateReadError != nil) { [pool release]; return noErr; } // compose the html html = [[NSMutableString alloc] initWithString:[NSString stringWithFormat:template, [[iconImage TIFFRepresentationUsingCompression:NSTIFFCompressionJPEG factor:0.01] base64EncodingWithLineLength:0], [NSString stringWithContentsOfFile:[myURL path] encoding:NSUTF8StringEncoding error:nil] ]]; } else if([[spf objectForKey:@"format"] isEqualToString:@"query favorites"]) { NSImage *iconImage; if([iconImages count] > 0) iconImage = [iconImages objectAtIndex:1]; else iconImage = [iconImages objectAtIndex:0]; template = [NSString stringWithContentsOfFile:[[NSBundle bundleWithIdentifier:@"com.google.code.sequel-pro.qlgenerator"] pathForResource:@"SPQLPluginQueryFavoritesTemplate" ofType:@"html"] encoding:NSUTF8StringEncoding error:&templateReadError]; if (template == nil || ![template length] || templateReadError != nil) { [pool release]; return noErr; } // compose the html html = [[NSMutableString alloc] initWithString:[NSString stringWithFormat:template, [[iconImage TIFFRepresentationUsingCompression:NSTIFFCompressionJPEG factor:0.01] base64EncodingWithLineLength:0], [NSString stringWithContentsOfFile:[myURL path] encoding:NSUTF8StringEncoding error:nil] ]]; } [spf release]; } else if([urlExtension isEqualToString:@"spfs"]) { NSImage *iconImage = [iconImages objectAtIndex:0]; template = [NSString stringWithContentsOfFile:[[NSBundle bundleWithIdentifier:@"com.google.code.sequel-pro.qlgenerator"] pathForResource:@"SPQLPluginConnectionBundleTemplate" ofType:@"html"] encoding:NSUTF8StringEncoding error:&templateReadError]; if (template == nil || ![template length] || templateReadError != nil) { [pool release]; return noErr; } NSString *windowTemplate = [NSString stringWithContentsOfFile:[[NSBundle bundleWithIdentifier:@"com.google.code.sequel-pro.qlgenerator"] pathForResource:@"SPQLPluginConnectionBundleWindowTemplate" ofType:@"html"] encoding:NSUTF8StringEncoding error:&templateReadError]; if (windowTemplate == nil || ![windowTemplate length] || templateReadError != nil) { [pool release]; return noErr; } NSError *readError = nil; NSString *convError = nil; NSPropertyListFormat format; NSDictionary *spf = nil; // Get info.plist data as dictionary NSData *pData = [NSData dataWithContentsOfFile:[NSString stringWithFormat:@"%@/info.plist", [myURL path]] options:NSUncachedRead error:&readError]; spf = [[NSPropertyListSerialization propertyListFromData:pData mutabilityOption:NSPropertyListImmutable format:&format errorDescription:&convError] retain]; if(!spf || readError != nil || [convError length] || !(format == NSPropertyListXMLFormat_v1_0 || format == NSPropertyListBinaryFormat_v1_0)) { if(spf) [spf release]; [pool release]; return noErr; } NSMutableString *spfsHTML = [NSMutableString string]; NSInteger connectionCounter = 0; NSArray *theWindows = [[[spf objectForKey:@"windows"] reverseObjectEnumerator] allObjects]; for(NSDictionary *window in theWindows) { NSInteger tabCounter = 0; NSInteger selectedTab = [[window objectForKey:@"selectedTabIndex"] integerValue]; [spfsHTML appendString:@"<table width='100%' border=1 style='border-collapse:collapse;border:2px solid lightgrey'>"]; NSArray *theTabs = [window objectForKey:@"tabs"]; for(NSDictionary *tab in theTabs) { connectionCounter++; if(tabCounter == selectedTab) [spfsHTML appendString:@"<tr><td style='background-color:#EEEEEE'>"]; else [spfsHTML appendString:@"<tr><td>"]; NSString *spfPath = @""; NSString *spfPathDisplay = @""; if([[tab objectForKey:@"isAbsolutePath"] boolValue]) { spfPath = [tab objectForKey:@"path"]; if([spfPath hasPrefix:NSHomeDirectory()]) { spfPathDisplay = [spfPath stringByReplacingOccurrencesOfString:NSHomeDirectory() withString:@"~"]; } else { spfPathDisplay = spfPath; } spfPathDisplay = [NSString stringWithFormat:@" (%@)", spfPathDisplay]; } else { spfPathDisplay = @""; spfPath = [NSString stringWithFormat:@"%@/Contents/%@", [myURL path], [tab objectForKey:@"path"]]; } if(spfPath == nil || ![spfPath length]) { [spfsHTML appendString:@" ∅"]; continue; } // Get info.plist data as dictionary NSDictionary *sessionSpf; NSData *pData = [NSData dataWithContentsOfFile:spfPath options:NSUncachedRead error:&readError]; sessionSpf = [[NSPropertyListSerialization propertyListFromData:pData mutabilityOption:NSPropertyListImmutable format:&format errorDescription:&convError] retain]; if(!sessionSpf || readError != nil || [convError length] || !(format == NSPropertyListXMLFormat_v1_0 || format == NSPropertyListBinaryFormat_v1_0)) { [spfsHTML appendFormat:@" %@ ∅", [tab objectForKey:@"path"]]; } else { NSString *name = @"••••"; NSString *host = @"••••"; NSString *user = @"••••"; NSString *database = @"••••"; if([[sessionSpf objectForKey:@"data"] isKindOfClass:[NSDictionary class]]) { if([[sessionSpf objectForKey:@"data"] objectForKey:@"connection"] && [[[sessionSpf objectForKey:@"data"] objectForKey:@"connection"] objectForKey:@"name"]) name = [[[sessionSpf objectForKey:@"data"] objectForKey:@"connection"] objectForKey:@"name"]; else name = @""; if([[sessionSpf objectForKey:@"data"] objectForKey:@"connection"] && [[[sessionSpf objectForKey:@"data"] objectForKey:@"connection"] objectForKey:@"host"]) host = [[[sessionSpf objectForKey:@"data"] objectForKey:@"connection"] objectForKey:@"host"]; else host = @""; if([[sessionSpf objectForKey:@"data"] objectForKey:@"connection"] && [[[sessionSpf objectForKey:@"data"] objectForKey:@"connection"] objectForKey:@"user"]) user = [[[sessionSpf objectForKey:@"data"] objectForKey:@"connection"] objectForKey:@"user"]; else user = @""; if([[sessionSpf objectForKey:@"data"] objectForKey:@"connection"] && [[[sessionSpf objectForKey:@"data"] objectForKey:@"connection"] objectForKey:@"database"]) database = [[[sessionSpf objectForKey:@"data"] objectForKey:@"connection"] objectForKey:@"database"]; else database = @""; } [spfsHTML appendFormat:windowTemplate, [sessionSpf objectForKey:@"rdbms_type"], [sessionSpf objectForKey:@"rdbms_version"], [name stringByReplacingOccurrencesOfString:@" " withString:@" "], spfPathDisplay, [host stringByReplacingOccurrencesOfString:@" " withString:@" "], [user stringByReplacingOccurrencesOfString:@" " withString:@" "], [database stringByReplacingOccurrencesOfString:@" " withString:@" "] ]; } tabCounter++; [spfsHTML appendString:@"</td></tr>"]; } [spfsHTML appendString:@"</table><br />"]; } if(connectionCounter > 1) previewHeight = 495; html = [[NSMutableString alloc] initWithString:[NSString stringWithFormat:template, [[iconImage TIFFRepresentationUsingCompression:NSTIFFCompressionJPEG factor:0.01] base64EncodingWithLineLength:0], connectionCounter, spfsHTML ]]; } else if([urlExtension isEqualToString:@"sql"]) { NSImage *iconImage = [iconImages objectAtIndex:0]; template = [NSString stringWithContentsOfFile:[[NSBundle bundleWithIdentifier:@"com.google.code.sequel-pro.qlgenerator"] pathForResource:@"SPQLPluginSQLTemplate" ofType:@"html"] encoding:NSUTF8StringEncoding error:&templateReadError]; if (template == nil || ![template length] || templateReadError != nil) { [pool release]; return noErr; } NSError *readError = nil; NSStringEncoding sqlEncoding = NSUTF8StringEncoding; if(fileAttributes) { NSNumber *filesize = [fileAttributes objectForKey:NSFileSize]; NSUInteger kMaxSQLFileSize = (0.7 * 1024 * 1024); // compose the html and perform syntax highlighting // read the file and try to get a proper encoding NSString *sqlText = [NSString stringWithContentsOfFile:[myURL path] encoding:sqlEncoding error:&readError]; NSMutableString *sqlHTML = [NSMutableString string]; NSString *truncatedString = @""; if(readError != nil) { // cocoa tries to detect the encoding sqlText = [NSString stringWithContentsOfFile:[myURL path] usedEncoding:&sqlEncoding error:&readError]; // fall back to latin1 if no sqlText couldn't read if(sqlText == nil) { sqlEncoding = NSISOLatin1StringEncoding; sqlText = [NSString stringWithContentsOfFile:[myURL path] encoding:sqlEncoding error:&readError]; } } // if nothing could be read print ... SQL ... if(!sqlText) { [sqlHTML appendString:@"... SQL ..."]; } else { // truncate large files since Finder blocks if([filesize unsignedLongValue] > kMaxSQLFileSize) { sqlText = [sqlText substringToIndex:kMaxSQLFileSize-1]; truncatedString = @"\n ✂ ..."; } NSRange textRange = NSMakeRange(0, [sqlText length]); NSString *tokenColor; size_t token; NSRange tokenRange; // initialise flex yyuoffset = 0; yyuleng = 0; yy_switch_to_buffer(yy_scan_string([sqlText UTF8String])); BOOL skipFontTag; // now loop through all the tokens while (token=yylex()){ skipFontTag = NO; switch (token) { case SPT_SINGLE_QUOTED_TEXT: case SPT_DOUBLE_QUOTED_TEXT: tokenColor = @"#A7221C"; break; case SPT_BACKTICK_QUOTED_TEXT: tokenColor = @"#001892"; break; case SPT_RESERVED_WORD: tokenColor = @"#0041F6"; break; case SPT_NUMERIC: tokenColor = @"#67350F"; break; case SPT_COMMENT: tokenColor = @"#265C10"; break; case SPT_VARIABLE: tokenColor = @"#6C6C6C"; break; case SPT_WHITESPACE: skipFontTag = YES; break; default: skipFontTag = YES; } tokenRange = NSMakeRange(yyuoffset, yyuleng); if(skipFontTag) [sqlHTML appendString:[sqlText substringWithRange:tokenRange]]; else [sqlHTML appendFormat:@"<font color=%@>%@</font>", tokenColor, [sqlText substringWithRange:tokenRange]]; } [sqlHTML appendString:truncatedString]; } html = [[NSMutableString alloc] initWithString:[NSString stringWithFormat:template, [[iconImage TIFFRepresentationUsingCompression:NSTIFFCompressionJPEG factor:0.01] base64EncodingWithLineLength:0], [NSString stringForByteSize:[[fileAttributes objectForKey:NSFileSize] longLongValue]], sqlHTML ]]; previewHeight = 495; } else { // No file attributes were read, bail for safety reasons [html release]; [pool release]; return noErr; } } NSMutableDictionary *props; props = [[[NSMutableDictionary alloc] init] autorelease]; [props setObject:[NSNumber numberWithInt:previewHeight] forKey:(NSString *)kQLPreviewPropertyHeightKey]; [props setObject:[NSNumber numberWithInt:600] forKey:(NSString *)kQLPreviewPropertyWidthKey]; QLPreviewRequestSetDataRepresentation(preview, (CFDataRef)[html dataUsingEncoding:NSUTF8StringEncoding], kUTTypeHTML, (CFDictionaryRef)props ); [html release]; [pool release]; return noErr; } void CancelPreviewGeneration(void* thisInterface, QLPreviewRequestRef preview) { // implement only if supported }