diff options
Diffstat (limited to 'Source')
-rw-r--r-- | Source/CustomQuery.h | 3 | ||||
-rw-r--r-- | Source/CustomQuery.m | 10 | ||||
-rw-r--r-- | Source/DeepMutableCopy.h | 10 | ||||
-rw-r--r-- | Source/ICUTemplateMatcher.h | 44 | ||||
-rw-r--r-- | Source/ICUTemplateMatcher.m | 192 | ||||
-rw-r--r-- | Source/MGTemplateEngine.h | 104 | ||||
-rw-r--r-- | Source/MGTemplateEngine.m | 673 | ||||
-rw-r--r-- | Source/MGTemplateFilter.h | 14 | ||||
-rw-r--r-- | Source/MGTemplateMarker.h | 41 | ||||
-rw-r--r-- | Source/MGTemplateStandardFilters.h | 15 | ||||
-rw-r--r-- | Source/MGTemplateStandardFilters.m | 97 | ||||
-rw-r--r-- | Source/MGTemplateStandardMarkers.h | 24 | ||||
-rw-r--r-- | Source/MGTemplateStandardMarkers.m | 620 | ||||
-rw-r--r-- | Source/NSArray_DeepMutableCopy.h | 12 | ||||
-rw-r--r-- | Source/NSArray_DeepMutableCopy.m | 42 | ||||
-rw-r--r-- | Source/NSDictionary_DeepMutableCopy.h | 12 | ||||
-rw-r--r-- | Source/NSDictionary_DeepMutableCopy.m | 43 | ||||
-rw-r--r-- | Source/RegexKitLite.h | 130 | ||||
-rw-r--r-- | Source/RegexKitLite.m | 354 | ||||
-rw-r--r-- | Source/TableContent.h | 4 | ||||
-rw-r--r-- | Source/TableContent.m | 22 | ||||
-rw-r--r-- | Source/TableDocument.h | 5 | ||||
-rw-r--r-- | Source/TableDocument.m | 122 | ||||
-rw-r--r-- | Source/TableSource.h | 1 | ||||
-rw-r--r-- | Source/TableSource.m | 19 |
25 files changed, 2602 insertions, 11 deletions
diff --git a/Source/CustomQuery.h b/Source/CustomQuery.h index c2eac75f..e2ef7e59 100644 --- a/Source/CustomQuery.h +++ b/Source/CustomQuery.h @@ -61,6 +61,8 @@ NSMutableArray *queryFavorites; CMMCPConnection *mySQLConnection; + + NSString *usedQuery; } // IBAction methods @@ -88,5 +90,6 @@ - (void)setConnection:(CMMCPConnection *)theConnection; - (void)setFavorites; - (void)doPerformQueryService:(NSString *)query; +- (NSString *)usedQuery; @end diff --git a/Source/CustomQuery.m b/Source/CustomQuery.m index 05eed36f..ba9349f9 100644 --- a/Source/CustomQuery.m +++ b/Source/CustomQuery.m @@ -396,6 +396,10 @@ sets the tableView columns corresponding to the mysql-result } } + if(usedQuery) + [usedQuery release]; + usedQuery = [[NSString stringWithString:[queries componentsJoinedByString:@";\n"]] retain]; + //perform empty query if no query is given if ( [queries count] == 0 ) { theResult = [mySQLConnection queryString:@""]; @@ -668,6 +672,10 @@ inserts the query in the textView and performs query [self runAllQueries:self]; } +- (NSString *)usedQuery +{ + return usedQuery; +} #pragma mark - #pragma mark TableView datasource methods @@ -1062,6 +1070,7 @@ traps enter key and { self = [super init]; prefs = nil; + usedQuery = [[NSString stringWithString:@""] retain]; return self; } @@ -1070,6 +1079,7 @@ traps enter key and [queryResult release]; [prefs release]; [queryFavorites release]; + [usedQuery release]; [super dealloc]; } diff --git a/Source/DeepMutableCopy.h b/Source/DeepMutableCopy.h new file mode 100644 index 00000000..a1c92e21 --- /dev/null +++ b/Source/DeepMutableCopy.h @@ -0,0 +1,10 @@ +/* + * DeepMutableCopy.h + * + * Created by Matt Gemmell on 02/05/2008. + * Copyright 2008 Instinctive Code. All rights reserved. + * + */ + +#import "NSArray_DeepMutableCopy.h" +#import "NSDictionary_DeepMutableCopy.h" diff --git a/Source/ICUTemplateMatcher.h b/Source/ICUTemplateMatcher.h new file mode 100644 index 00000000..d02c0a31 --- /dev/null +++ b/Source/ICUTemplateMatcher.h @@ -0,0 +1,44 @@ +// +// ICUTemplateMatcher.h +// +// Created by Matt Gemmell on 19/05/2008. +// Copyright 2008 Instinctive Code. All rights reserved. +// + +#import "MGTemplateEngine.h" + +/* + This is an example Matcher for MGTemplateEngine, implemented using libicucore on Leopard, + via the RegexKitLite library: http://regexkit.sourceforge.net/#RegexKitLite + + This project includes everything you need, as long as you're building on Mac OS X 10.5 or later. + + Other matchers can easily be implemented using the MGTemplateEngineMatcher protocol, + if you prefer to use another regex framework, or use another matching method entirely. + */ + +@interface ICUTemplateMatcher : NSObject <MGTemplateEngineMatcher> { + MGTemplateEngine *engine; + NSString *markerStart; + NSString *markerEnd; + NSString *exprStart; + NSString *exprEnd; + NSString *filterDelimiter; + NSString *templateString; + NSString *regex; +} + +@property(assign) MGTemplateEngine *engine; // weak ref +@property(retain) NSString *markerStart; +@property(retain) NSString *markerEnd; +@property(retain) NSString *exprStart; +@property(retain) NSString *exprEnd; +@property(retain) NSString *filterDelimiter; +@property(retain) NSString *templateString; +@property(retain) NSString *regex; + ++ (ICUTemplateMatcher *)matcherWithTemplateEngine:(MGTemplateEngine *)theEngine; + +- (NSArray *)argumentsFromString:(NSString *)argString; + +@end diff --git a/Source/ICUTemplateMatcher.m b/Source/ICUTemplateMatcher.m new file mode 100644 index 00000000..dbeb49aa --- /dev/null +++ b/Source/ICUTemplateMatcher.m @@ -0,0 +1,192 @@ +// +// ICUTemplateMatcher.m +// +// Created by Matt Gemmell on 19/05/2008. +// Copyright 2008 Instinctive Code. All rights reserved. +// + +#import "ICUTemplateMatcher.h" +#import "RegexKitLite.h" + + +@implementation ICUTemplateMatcher + + ++ (ICUTemplateMatcher *)matcherWithTemplateEngine:(MGTemplateEngine *)theEngine +{ + return [[[ICUTemplateMatcher alloc] initWithTemplateEngine:theEngine] autorelease]; +} + + +- (id)initWithTemplateEngine:(MGTemplateEngine *)theEngine +{ + if (self = [super init]) { + self.engine = theEngine; // weak ref + } + + return self; +} + + +- (void)dealloc +{ + self.engine = nil; + self.templateString = nil; + self.markerStart = nil; + self.markerEnd = nil; + self.exprStart = nil; + self.exprEnd = nil; + self.filterDelimiter = nil; + self.regex = nil; + + [super dealloc]; +} + + +- (void)engineSettingsChanged +{ + // This method is a good place to cache settings from the engine. + self.markerStart = engine.markerStartDelimiter; + self.markerEnd = engine.markerEndDelimiter; + self.exprStart = engine.expressionStartDelimiter; + self.exprEnd = engine.expressionEndDelimiter; + self.filterDelimiter = engine.filterDelimiter; + self.templateString = engine.templateContents; + + // Note: the \Q ... \E syntax causes everything inside it to be treated as literals. + // This help us in the case where the marker/filter delimiters have special meaning + // in regular expressions; notably the "$" character in the default marker start-delimiter. + // Note: the (?m) syntax makes ICU enable multiline matching. + NSString *basePattern = @"(\\Q%@\\E)(?:\\s+)?(.*?)(?:(?:\\s+)?\\Q%@\\E(?:\\s+)?(.*?))?(?:\\s+)?\\Q%@\\E"; + NSString *mrkrPattern = [NSString stringWithFormat:basePattern, self.markerStart, self.filterDelimiter, self.markerEnd]; + NSString *exprPattern = [NSString stringWithFormat:basePattern, self.exprStart, self.filterDelimiter, self.exprEnd]; + self.regex = [NSString stringWithFormat:@"(?m)(?:%@|%@)", mrkrPattern, exprPattern]; +} + + +- (NSDictionary *)firstMarkerWithinRange:(NSRange)range +{ + NSRange matchRange = [self.templateString rangeOfRegex:self.regex options:RKLNoOptions inRange:range capture:0 error:NULL]; + NSMutableDictionary *markerInfo = nil; + if (matchRange.length > 0) { + markerInfo = [NSMutableDictionary dictionary]; + [markerInfo setObject:[NSValue valueWithRange:matchRange] forKey:MARKER_RANGE_KEY]; + + // Found a match. Obtain marker string. + NSString *matchString = [self.templateString substringWithRange:matchRange]; + NSRange localRange = NSMakeRange(0, [matchString length]); + //NSLog(@"mtch: \"%@\"", matchString); + + // Find type of match + NSString *matchType = nil; + NSRange mrkrSubRange = [matchString rangeOfRegex:regex options:RKLNoOptions inRange:localRange capture:1 error:NULL]; + BOOL isMarker = (mrkrSubRange.length > 0); // only matches if match has marker-delimiters + int offset = 0; + if (isMarker) { + matchType = MARKER_TYPE_MARKER; + } else { + matchType = MARKER_TYPE_EXPRESSION; + offset = 3; + } + [markerInfo setObject:matchType forKey:MARKER_TYPE_KEY]; + + // Split marker string into marker-name and arguments. + NSRange markerRange = NSMakeRange(0, [matchString length]); + markerRange = [matchString rangeOfRegex:regex options:RKLNoOptions inRange:localRange capture:2 + offset error:NULL]; + + if (markerRange.length > 0) { + NSString *markerString = [matchString substringWithRange:markerRange]; + NSArray *markerComponents = [self argumentsFromString:markerString]; + if (markerComponents && [markerComponents count] > 0) { + [markerInfo setObject:[markerComponents objectAtIndex:0] forKey:MARKER_NAME_KEY]; + int count = [markerComponents count]; + if (count > 1) { + [markerInfo setObject:[markerComponents subarrayWithRange:NSMakeRange(1, count - 1)] + forKey:MARKER_ARGUMENTS_KEY]; + } + } + + // Check for filter. + NSRange filterRange = [matchString rangeOfRegex:regex options:RKLNoOptions inRange:localRange capture:3 + offset error:NULL]; + if (filterRange.length > 0) { + // Found a filter. Obtain filter string. + NSString *filterString = [matchString substringWithRange:filterRange]; + + // Convert first : plus any immediately-following whitespace into a space. + localRange = NSMakeRange(0, [filterString length]); + NSString *space = @" "; + NSRange filterArgDelimRange = [filterString rangeOfRegex:@":(?:\\s+)?" options:RKLNoOptions inRange:localRange + capture:0 error:NULL]; + if (filterArgDelimRange.length > 0) { + // Replace found text with space. + filterString = [NSString stringWithFormat:@"%@%@%@", + [filterString substringWithRange:NSMakeRange(0, filterArgDelimRange.location)], + space, + [filterString substringWithRange:NSMakeRange(NSMaxRange(filterArgDelimRange), + localRange.length - NSMaxRange(filterArgDelimRange))]]; + } + + // Split into filter-name and arguments. + NSArray *filterComponents = [self argumentsFromString:filterString]; + if (filterComponents && [filterComponents count] > 0) { + [markerInfo setObject:[filterComponents objectAtIndex:0] forKey:MARKER_FILTER_KEY]; + int count = [filterComponents count]; + if (count > 1) { + [markerInfo setObject:[filterComponents subarrayWithRange:NSMakeRange(1, count - 1)] + forKey:MARKER_FILTER_ARGUMENTS_KEY]; + } + } + } + } + } + + return markerInfo; +} + + +- (NSArray *)argumentsFromString:(NSString *)argString +{ + // Extract arguments from argString, taking care not to break single- or double-quoted arguments, + // including those containing \-escaped quotes. + NSString *argsPattern = @"\"(.*?)(?<!\\\\)\"|'(.*?)(?<!\\\\)'|(\\S+)"; + NSMutableArray *args = [NSMutableArray array]; + + int location = 0; + while (location != NSNotFound) { + NSRange searchRange = NSMakeRange(location, [argString length] - location); + NSRange entireRange = [argString rangeOfRegex:argsPattern options:RKLNoOptions + inRange:searchRange capture:0 error:NULL]; + NSRange matchedRange = [argString rangeOfRegex:argsPattern options:RKLNoOptions + inRange:searchRange capture:1 error:NULL]; + if (matchedRange.length == 0) { + matchedRange = [argString rangeOfRegex:argsPattern options:RKLNoOptions + inRange:searchRange capture:2 error:NULL]; + if (matchedRange.length == 0) { + matchedRange = [argString rangeOfRegex:argsPattern options:RKLNoOptions + inRange:searchRange capture:3 error:NULL]; + } + } + + location = NSMaxRange(entireRange) + ((entireRange.length == 0) ? 1 : 0); + if (matchedRange.length > 0) { + [args addObject:[argString substringWithRange:matchedRange]]; + } else { + location = NSNotFound; + } + } + + return args; +} + + +@synthesize engine; +@synthesize markerStart; +@synthesize markerEnd; +@synthesize exprStart; +@synthesize exprEnd; +@synthesize filterDelimiter; +@synthesize templateString; +@synthesize regex; + + +@end diff --git a/Source/MGTemplateEngine.h b/Source/MGTemplateEngine.h new file mode 100644 index 00000000..f0eb9a43 --- /dev/null +++ b/Source/MGTemplateEngine.h @@ -0,0 +1,104 @@ +// +// MGTemplateEngine.h +// +// Created by Matt Gemmell on 11/05/2008. +// Copyright 2008 Instinctive Code. All rights reserved. +// + +// Keys in blockInfo dictionaries passed to delegate methods. +#define BLOCK_NAME_KEY @"name" // NSString containing block name (first word of marker) +#define BLOCK_END_NAMES_KEY @"endNames" // NSArray containing names of possible ending-markers for block +#define BLOCK_ARGUMENTS_KEY @"args" // NSArray of further arguments in block start marker +#define BLOCK_START_MARKER_RANGE_KEY @"startMarkerRange" // NSRange (as NSValue) of block's starting marker +#define BLOCK_VARIABLES_KEY @"vars" // NSDictionary of variables + +#define TEMPLATE_ENGINE_ERROR_DOMAIN @"MGTemplateEngineErrorDomain" + +@class MGTemplateEngine; +@protocol MGTemplateEngineDelegate +@optional +- (void)templateEngine:(MGTemplateEngine *)engine blockStarted:(NSDictionary *)blockInfo; +- (void)templateEngine:(MGTemplateEngine *)engine blockEnded:(NSDictionary *)blockInfo; +- (void)templateEngineFinishedProcessingTemplate:(MGTemplateEngine *)engine; +- (void)templateEngine:(MGTemplateEngine *)engine encounteredError:(NSError *)error isContinuing:(BOOL)continuing; +@end + +// Keys in marker dictionaries returned from Matcher methods. +#define MARKER_NAME_KEY @"name" // NSString containing marker name (first word of marker) +#define MARKER_TYPE_KEY @"type" // NSString, either MARKER_TYPE_EXPRESSION or MARKER_TYPE_MARKER +#define MARKER_TYPE_MARKER @"marker" +#define MARKER_TYPE_EXPRESSION @"expression" +#define MARKER_ARGUMENTS_KEY @"args" // NSArray of further arguments in marker, if any +#define MARKER_FILTER_KEY @"filter" // NSString containing name of filter attached to marker, if any +#define MARKER_FILTER_ARGUMENTS_KEY @"filterArgs" // NSArray of filter arguments, if any +#define MARKER_RANGE_KEY @"range" // NSRange (as NSValue) of marker's range + +@protocol MGTemplateEngineMatcher +@required +- (id)initWithTemplateEngine:(MGTemplateEngine *)engine; +- (void)engineSettingsChanged; // always called at least once before beginning to process a template. +- (NSDictionary *)firstMarkerWithinRange:(NSRange)range; +@end + +#import "MGTemplateMarker.h" +#import "MGTemplateFilter.h" + +@interface MGTemplateEngine : NSObject { +@public + NSString *markerStartDelimiter; // default: {% + NSString *markerEndDelimiter; // default: %} + NSString *expressionStartDelimiter; // default: {{ + NSString *expressionEndDelimiter; // default: }} + NSString *filterDelimiter; // default: | example: {{ myVar|uppercase }} + NSString *literalStartMarker; // default: literal + NSString *literalEndMarker; // default: /literal +@private + NSMutableArray *_openBlocksStack; + NSMutableDictionary *_globals; + int _outputDisabledCount; + int _templateLength; + NSMutableDictionary *_filters; + NSMutableDictionary *_markers; + NSMutableDictionary *_templateVariables; + BOOL _literal; +@public + NSRange remainingRange; + id <MGTemplateEngineDelegate> delegate; + id <MGTemplateEngineMatcher> matcher; + NSString *templateContents; +} + +@property(retain) NSString *markerStartDelimiter; +@property(retain) NSString *markerEndDelimiter; +@property(retain) NSString *expressionStartDelimiter; +@property(retain) NSString *expressionEndDelimiter; +@property(retain) NSString *filterDelimiter; +@property(retain) NSString *literalStartMarker; +@property(retain) NSString *literalEndMarker; +@property(assign, readonly) NSRange remainingRange; +@property(assign) id <MGTemplateEngineDelegate> delegate; // weak ref +@property(retain) id <MGTemplateEngineMatcher> matcher; +@property(retain, readonly) NSString *templateContents; + +// Creation. ++ (NSString *)version; ++ (MGTemplateEngine *)templateEngine; + +// Managing persistent values. +- (void)setObject:(id)anObject forKey:(id)aKey; +- (void)addEntriesFromDictionary:(NSDictionary *)dict; +- (id)objectForKey:(id)aKey; + +// Configuration and extensibility. +- (void)loadMarker:(NSObject <MGTemplateMarker> *)marker; +- (void)loadFilter:(NSObject <MGTemplateFilter> *)filter; + +// Utilities. +- (NSObject *)resolveVariable:(NSString *)var; +- (NSDictionary *)templateVariables; + +// Processing templates. +- (NSString *)processTemplate:(NSString *)templateString withVariables:(NSDictionary *)variables; +- (NSString *)processTemplateInFileAtPath:(NSString *)templatePath withVariables:(NSDictionary *)variables; + +@end diff --git a/Source/MGTemplateEngine.m b/Source/MGTemplateEngine.m new file mode 100644 index 00000000..bf99afec --- /dev/null +++ b/Source/MGTemplateEngine.m @@ -0,0 +1,673 @@ +// +// MGTemplateEngine.m +// +// Created by Matt Gemmell on 11/05/2008. +// Copyright 2008 Instinctive Code. All rights reserved. +// + +#import "MGTemplateEngine.h" +#import "MGTemplateStandardMarkers.h" +#import "MGTemplateStandardFilters.h" +#import "DeepMutableCopy.h" + + +#define DEFAULT_MARKER_START @"{%" +#define DEFAULT_MARKER_END @"%}" +#define DEFAULT_EXPRESSION_START @"{{" // should always be different from marker-start +#define DEFAULT_EXPRESSION_END @"}}" +#define DEFAULT_FILTER_START @"|" +#define DEFAULT_LITERAL_START @"literal" +#define DEFAULT_LITERAL_END @"/literal" +// example: {% markername arg1 arg2|filter:arg1 arg2 %} + +#define GLOBAL_ENGINE_GROUP @"engine" // name of dictionary in globals containing engine settings +#define GLOBAL_ENGINE_DELIMITERS @"delimiters" // name of dictionary in GLOBAL_ENGINE_GROUP containing delimiters +#define GLOBAL_DELIM_MARKER_START @"markerStart" // name of key in GLOBAL_ENGINE_DELIMITERS containing marker start delimiter +#define GLOBAL_DELIM_MARKER_END @"markerEnd" +#define GLOBAL_DELIM_EXPR_START @"expressionStart" +#define GLOBAL_DELIM_EXPR_END @"expressionEnd" +#define GLOBAL_DELIM_FILTER @"filter" + +@interface MGTemplateEngine (PrivateMethods) + +- (NSObject *)valueForVariable:(NSString *)var parent:(NSObject **)parent parentKey:(NSString **)parentKey; +- (void)setValue:(NSObject *)newValue forVariable:(NSString *)var forceCurrentStackFrame:(BOOL)inStackFrame; +- (void)reportError:(NSString *)errorStr code:(int)code continuing:(BOOL)continuing; +- (void)reportBlockBoundaryStarted:(BOOL)started; +- (void)reportTemplateProcessingFinished; + +@end + + +@implementation MGTemplateEngine + + +#pragma mark Creation and destruction + + ++ (NSString *)version +{ + // 1.0.0 20 May 2008 + return @"1.0.0"; +} + + ++ (MGTemplateEngine *)templateEngine +{ + return [[[MGTemplateEngine alloc] init] autorelease]; +} + + +- (id)init +{ + if (self = [super init]) { + _openBlocksStack = [[NSMutableArray alloc] init]; + _globals = [[NSMutableDictionary alloc] init]; + _markers = [[NSMutableDictionary alloc] init]; + _filters = [[NSMutableDictionary alloc] init]; + _templateVariables = [[NSMutableDictionary alloc] init]; + _outputDisabledCount = 0; // i.e. not disabled. + self.markerStartDelimiter = DEFAULT_MARKER_START; + self.markerEndDelimiter = DEFAULT_MARKER_END; + self.expressionStartDelimiter = DEFAULT_EXPRESSION_START; + self.expressionEndDelimiter = DEFAULT_EXPRESSION_END; + self.filterDelimiter = DEFAULT_FILTER_START; + self.literalStartMarker = DEFAULT_LITERAL_START; + self.literalEndMarker = DEFAULT_LITERAL_END; + + // Load standard markers and filters. + [self loadMarker:[[[MGTemplateStandardMarkers alloc] initWithTemplateEngine:self] autorelease]]; + [self loadFilter:[[[MGTemplateStandardFilters alloc] init] autorelease]]; + } + + return self; +} + + +- (void)dealloc +{ + [_openBlocksStack release]; + _openBlocksStack = nil; + [_globals release]; + _globals = nil; + [_filters release]; + _filters = nil; + [_markers release]; + _markers = nil; + self.delegate = nil; + [templateContents release]; + templateContents = nil; + [_templateVariables release]; + _templateVariables = nil; + self.markerStartDelimiter = nil; + self.markerEndDelimiter = nil; + self.expressionStartDelimiter = nil; + self.expressionEndDelimiter = nil; + self.filterDelimiter = nil; + self.literalStartMarker = nil; + self.literalEndMarker = nil; + + [super dealloc]; +} + + +#pragma mark Managing persistent values. + + +- (void)setObject:(id)anObject forKey:(id)aKey +{ + [_globals setObject:anObject forKey:aKey]; +} + + +- (void)addEntriesFromDictionary:(NSDictionary *)dict +{ + [_globals addEntriesFromDictionary:dict]; +} + + +- (id)objectForKey:(id)aKey +{ + return [_globals objectForKey:aKey]; +} + + +#pragma mark Configuration and extensibility. + + +- (void)loadMarker:(NSObject <MGTemplateMarker> *)marker +{ + if (marker) { + // Obtain claimed markers. + NSArray *markers = [marker markers]; + if (markers) { + for (NSString *markerName in markers) { + NSObject *existingHandler = [_markers objectForKey:markerName]; + if (!existingHandler) { + // Set this MGTemplateMaker instance as the handler for markerName. + [_markers setObject:marker forKey:markerName]; + } + } + } + } +} + + +- (void)loadFilter:(NSObject <MGTemplateFilter> *)filter +{ + if (filter) { + // Obtain claimed filters. + NSArray *filters = [filter filters]; + if (filters) { + for (NSString *filterName in filters) { + NSObject *existingHandler = [_filters objectForKey:filterName]; + if (!existingHandler) { + // Set this MGTemplateFilter instance as the handler for filterName. + [_filters setObject:filter forKey:filterName]; + } + } + } + } +} + + +#pragma mark Delegate + + +- (void)reportError:(NSString *)errorStr code:(int)code continuing:(BOOL)continuing +{ + if (delegate) { + NSString *errStr = NSLocalizedString(errorStr, nil); + if (!continuing) { + errStr = [NSString stringWithFormat:@"%@: %@", NSLocalizedString(@"Fatal Error", nil), errStr]; + } + SEL selector = @selector(templateEngine:encounteredError:isContinuing:); + if ([(NSObject *)delegate respondsToSelector:selector]) { + NSError *error = [NSError errorWithDomain:TEMPLATE_ENGINE_ERROR_DOMAIN + code:code + userInfo:[NSDictionary dictionaryWithObject:errStr + forKey:NSLocalizedDescriptionKey]]; + [(NSObject <MGTemplateEngineDelegate> *)delegate templateEngine:self + encounteredError:error + isContinuing:continuing]; + } + } +} + + +- (void)reportBlockBoundaryStarted:(BOOL)started +{ + if (delegate) { + SEL selector = (started) ? @selector(templateEngine:blockStarted:) : @selector(templateEngine:blockEnded:); + if ([(NSObject *)delegate respondsToSelector:selector]) { + [(NSObject *)delegate performSelector:selector withObject:self withObject:[_openBlocksStack lastObject]]; + } + } +} + + +- (void)reportTemplateProcessingFinished +{ + if (delegate) { + SEL selector = @selector(templateEngineFinishedProcessingTemplate:); + if ([(NSObject *)delegate respondsToSelector:selector]) { + [(NSObject *)delegate performSelector:selector withObject:self]; + } + } +} + + +#pragma mark Utilities. + + +- (NSObject *)valueForVariable:(NSString *)var parent:(NSObject **)parent parentKey:(NSString **)parentKey +{ + // Returns value for given variable-path, and returns by reference the parent object the variable + // is contained in, and the key used on that parent object to access the variable. + // e.g. for var "thing.stuff.2", where thing = NSDictionary and stuff = NSArray, + // parent would be a pointer to the "stuff" array, and parentKey would be "2". + + NSString *dot = @"."; + NSArray *dotBits = [var componentsSeparatedByString:dot]; + NSObject *result = nil; + NSObject *currObj = nil; + + // Check to see if there's a top-level entry for first part of var in templateVariables. + NSString *firstVar = [dotBits objectAtIndex:0]; + + if ([_templateVariables objectForKey:firstVar]) { + currObj = _templateVariables; + } else if ([_globals objectForKey:firstVar]) { + currObj = _globals; + } else { + // Attempt to find firstVar in stack variables. + NSEnumerator *stack = [_openBlocksStack reverseObjectEnumerator]; + NSDictionary *stackFrame = nil; + while (stackFrame = [stack nextObject]) { + NSDictionary *vars = [stackFrame objectForKey:BLOCK_VARIABLES_KEY]; + if (vars && [vars objectForKey:firstVar]) { + currObj = vars; + break; + } + } + } + + if (!currObj) { + return nil; + } + + // Try raw KVC. + @try { + result = [currObj valueForKeyPath:var]; + } + @catch (NSException *exception) { + // do nothing + } + + if (result) { + // Got it with regular KVC. Work out parent and parentKey if necessary. + if (parent || parentKey) { + if ([dotBits count] > 1) { + if (parent) { + *parent = [currObj valueForKeyPath:[[dotBits subarrayWithRange:NSMakeRange(0, [dotBits count] - 1)] + componentsJoinedByString:dot]]; + } + if (parentKey) { + *parentKey = [dotBits lastObject]; + } + } else { + if (parent) { + *parent = currObj; + } + if (parentKey) { + *parentKey = var; + } + } + } + } else { + // Try iterative checking for array indices. + int numKeys = [dotBits count]; + if (numKeys > 1) { // otherwise no point in checking + NSObject *thisParent = currObj; + NSString *thisKey = nil; + for (int i = 0; i < numKeys; i++) { + thisKey = [dotBits objectAtIndex:i]; + NSObject *newObj = nil; + @try { + newObj = [currObj valueForKeyPath:thisKey]; + } + @catch (NSException *e) { + // do nothing + } + // Check to see if this is an array which we can index into. + if (!newObj && [currObj isKindOfClass:[NSArray class]]) { + NSCharacterSet *numbersSet = [NSCharacterSet decimalDigitCharacterSet]; + NSScanner *scanner = [NSScanner scannerWithString:thisKey]; + NSString *digits; + BOOL scanned = [scanner scanCharactersFromSet:numbersSet intoString:&digits]; + if (scanned && digits && [digits length] > 0) { + int index = [digits intValue]; + if (index >= 0 && index < [((NSArray *)currObj) count]) { + newObj = [((NSArray *)currObj) objectAtIndex:index]; + } + } + } + thisParent = currObj; + currObj = newObj; + if (!currObj) { + break; + } + } + result = currObj; + if (parent || parentKey) { + if (parent) { + *parent = thisParent; + } + if (parentKey) { + *parentKey = thisKey; + } + } + } + } + + return result; +} + + +- (void)setValue:(NSObject *)newValue forVariable:(NSString *)var forceCurrentStackFrame:(BOOL)inStackFrame +{ + NSObject *parent = nil; + NSString *parentKey = nil; + NSObject *currValue; + currValue = [self valueForVariable:var parent:&parent parentKey:&parentKey]; + if (!inStackFrame && currValue && (currValue != newValue)) { + // Set new value appropriately. + if ([parent isKindOfClass:[NSMutableArray class]]) { + [(NSMutableArray *)parent replaceObjectAtIndex:[parentKey intValue] withObject:newValue]; + } else { + // Try using setValue:forKey: + @try { + [parent setValue:newValue forKey:parentKey]; + } + @catch (NSException *e) { + // do nothing + } + } + } else if (!currValue || inStackFrame) { + // Put the variable into the current block-stack frame, or _templateVariables otherwise. + NSMutableDictionary *vars; + if ([_openBlocksStack count] > 0) { + vars = [[_openBlocksStack lastObject] objectForKey:BLOCK_VARIABLES_KEY]; + } else { + vars = _templateVariables; + } + if ([vars respondsToSelector:@selector(setValue:forKey:)]) { + [vars setValue:newValue forKey:var]; + } + } +} + + +- (NSObject *)resolveVariable:(NSString *)var +{ + NSObject *parent = nil; + NSString *key = nil; + NSObject *result = [self valueForVariable:var parent:&parent parentKey:&key]; + //NSLog(@"var: %@, parent: %@, key: %@, result: %@", var, parent, key, result); + return result; +} + + +- (NSDictionary *)templateVariables +{ + return [NSDictionary dictionaryWithDictionary:_templateVariables]; +} + + +#pragma mark Processing templates. + + +- (NSString *)processTemplate:(NSString *)templateString withVariables:(NSDictionary *)variables +{ + // Set up environment. + [_openBlocksStack release]; + _openBlocksStack = [[NSMutableArray alloc] init]; + [_globals setObject:[NSDictionary dictionaryWithObjectsAndKeys: + [NSDictionary dictionaryWithObjectsAndKeys: + self.markerStartDelimiter, GLOBAL_DELIM_MARKER_START, + self.markerEndDelimiter, GLOBAL_DELIM_MARKER_END, + self.expressionStartDelimiter, GLOBAL_DELIM_EXPR_START, + self.expressionEndDelimiter, GLOBAL_DELIM_EXPR_END, + self.filterDelimiter, GLOBAL_DELIM_FILTER, + nil], GLOBAL_ENGINE_DELIMITERS, + nil] + forKey:GLOBAL_ENGINE_GROUP]; + [_globals setObject:[NSNumber numberWithBool:YES] forKey:@"true"]; + [_globals setObject:[NSNumber numberWithBool:NO] forKey:@"false"]; + [_globals setObject:[NSNumber numberWithBool:YES] forKey:@"YES"]; + [_globals setObject:[NSNumber numberWithBool:NO] forKey:@"NO"]; + [_globals setObject:[NSNumber numberWithBool:YES] forKey:@"yes"]; + [_globals setObject:[NSNumber numberWithBool:NO] forKey:@"no"]; + _outputDisabledCount = 0; + [templateContents release]; + templateContents = [templateString retain]; + _templateLength = [templateString length]; + [_templateVariables release]; + _templateVariables = [variables deepMutableCopy]; + remainingRange = NSMakeRange(0, [templateString length]); + _literal = NO; + + // Ensure we have a matcher. + if (!matcher) { + [self reportError:@"No matcher has been configured for the template engine" code:7 continuing:NO]; + return nil; + } + + // Tell our matcher to take note of our settings. + [matcher engineSettingsChanged]; + NSMutableString *output = [NSMutableString string]; + + while (remainingRange.location != NSNotFound) { + NSDictionary *matchInfo = [matcher firstMarkerWithinRange:remainingRange]; + if (matchInfo) { + // Append output before marker if appropriate. + NSRange matchRange = [[matchInfo objectForKey:MARKER_RANGE_KEY] rangeValue]; + if (_outputDisabledCount == 0) { + NSRange preMarkerRange = NSMakeRange(remainingRange.location, matchRange.location - remainingRange.location); + [output appendFormat:@"%@", [templateContents substringWithRange:preMarkerRange]]; + } + + // Adjust remainingRange. + remainingRange.location = NSMaxRange(matchRange); + remainingRange.length = _templateLength - remainingRange.location; + + // Process the marker we found. + //NSLog(@"Match: %@", matchInfo); + NSString *matchMarker = [matchInfo objectForKey:MARKER_NAME_KEY]; + + // Deal with literal mode. + if ([matchMarker isEqualToString:self.literalStartMarker]) { + if (_literal && _outputDisabledCount == 0) { + // Output this tag literally. + [output appendFormat:@"%@", [templateContents substringWithRange:matchRange]]; + } else { + // Enable literal mode. + _literal = YES; + } + continue; + } else if ([matchMarker isEqualToString:self.literalEndMarker]) { + // Disable literal mode. + _literal = NO; + continue; + } else if (_literal && _outputDisabledCount == 0) { + [output appendFormat:@"%@", [templateContents substringWithRange:matchRange]]; + continue; + } + + // Check to see if the match is a marker. + BOOL isMarker = [[matchInfo objectForKey:MARKER_TYPE_KEY] isEqualToString:MARKER_TYPE_MARKER]; + NSObject <MGTemplateMarker> *markerHandler = nil; + NSObject *val = nil; + if (isMarker) { + markerHandler = [_markers objectForKey:matchMarker]; + + // Process marker with handler. + BOOL blockStarted = NO; + BOOL blockEnded = NO; + BOOL outputEnabled = (_outputDisabledCount == 0); + BOOL outputWasEnabled = outputEnabled; + NSRange nextRange = remainingRange; + NSDictionary *newVariables = nil; + NSDictionary *blockInfo = nil; + + // If markerHandler is same as that of current block, send blockInfo. + if ([_openBlocksStack count] > 0) { + NSDictionary *currBlock = [_openBlocksStack lastObject]; + NSString *currBlockStartMarker = [currBlock objectForKey:BLOCK_NAME_KEY]; + if ([_markers objectForKey:currBlockStartMarker] == markerHandler) { + blockInfo = currBlock; + } + } + + // Call marker's handler. + val = [markerHandler markerEncountered:matchMarker + withArguments:[matchInfo objectForKey:MARKER_ARGUMENTS_KEY] + inRange:matchRange + blockStarted:&blockStarted blockEnded:&blockEnded + outputEnabled:&outputEnabled nextRange:&nextRange + currentBlockInfo:blockInfo newVariables:&newVariables]; + + if (outputEnabled != outputWasEnabled) { + if (outputEnabled) { + _outputDisabledCount--; + } else { + _outputDisabledCount++; + } + } + remainingRange = nextRange; + + // Check to see if remainingRange is valid. + if (NSMaxRange(remainingRange) > [self.templateContents length]) { + [self reportError:[NSString stringWithFormat:@"Marker handler \"%@\" specified an invalid range to resume processing from", + matchMarker] + code:5 continuing:NO]; + break; + } + + BOOL forceVarsToStack = NO; + if (blockStarted && blockEnded) { + // This is considered an error on the part of the marker-handler. Report to delegate. + [self reportError:[NSString stringWithFormat:@"Marker \"%@\" reported that a block simultaneously began and ended", + matchMarker] + code:0 continuing:YES]; + } else if (blockStarted) { + NSArray *endMarkers = [markerHandler endMarkersForMarker:matchMarker]; + if (!endMarkers) { + // Report error to delegate. + [self reportError:[NSString stringWithFormat:@"Marker \"%@\" started a block but did not supply any suitable end-markers", + matchMarker] + code:4 continuing:YES]; + continue; + } + + // A block has begun. Create relevant stack frame. + NSMutableDictionary *frame = [NSMutableDictionary dictionary]; + [frame setObject:matchMarker forKey:BLOCK_NAME_KEY]; + [frame setObject:endMarkers forKey:BLOCK_END_NAMES_KEY]; + NSArray *arguments = [matchInfo objectForKey:MARKER_ARGUMENTS_KEY]; + if (!arguments) { + arguments = [NSArray array]; + } + [frame setObject:arguments forKey:BLOCK_ARGUMENTS_KEY]; + [frame setObject:[matchInfo objectForKey:MARKER_RANGE_KEY] forKey:BLOCK_START_MARKER_RANGE_KEY]; + [frame setObject:[NSMutableDictionary dictionary] forKey:BLOCK_VARIABLES_KEY]; + [_openBlocksStack addObject:frame]; + + forceVarsToStack = YES; + + // Report block start to delegate. + [self reportBlockBoundaryStarted:YES]; + } else if (blockEnded) { + if (!blockInfo || + ([_openBlocksStack count] > 0 && + ![(NSArray *)[[_openBlocksStack lastObject] objectForKey:BLOCK_END_NAMES_KEY] containsObject:matchMarker])) { + // The marker-handler just told us a block ended, but the current block was not + // started by that marker-handler. This means a syntax error exists in the template, + // specifically an unterminated block (the current block). + // This is considered an unrecoverable error. + NSString *errMsg; + if ([_openBlocksStack count] == 0) { + errMsg = [NSString stringWithFormat:@"Marker \"%@\" reported that a non-existent block ended", + matchMarker]; + } else { + NSString *currBlockName = [[_openBlocksStack lastObject] objectForKey:BLOCK_NAME_KEY]; + errMsg = [NSString stringWithFormat:@"Marker \"%@\" reported that a block ended, \ +but current block was started by \"%@\" marker", + matchMarker, currBlockName]; + } + [self reportError:errMsg code:1 continuing:YES]; + break; + } + + // Report block end to delegate before removing stack frame, so we can send info dict. + [self reportBlockBoundaryStarted:NO]; + + // Remove relevant stack frame. + if ([_openBlocksStack count] > 0) { + [_openBlocksStack removeLastObject]; + } + } + + // Process newVariables + if (newVariables) { + //NSLog(@"new vars %@", newVariables); + for (NSString *key in newVariables) { + [self setValue:[newVariables objectForKey:key] forVariable:key forceCurrentStackFrame:forceVarsToStack]; + } + } + + } else { + // Check to see if the first word of the match is a variable. + val = [self resolveVariable:matchMarker]; + } + + // Prepare result for output, if we have a result. + if (val && _outputDisabledCount == 0) { + // Process filter if specified. + NSString *filter = [matchInfo objectForKey:MARKER_FILTER_KEY]; + if (filter) { + NSObject <MGTemplateFilter> *filterHandler = [_filters objectForKey:filter]; + if (filterHandler) { + val = [filterHandler filterInvoked:filter + withArguments:[matchInfo objectForKey:MARKER_FILTER_ARGUMENTS_KEY] onValue:val]; + } + } + + // Output result. + [output appendFormat:@"%@", val]; + } else if ((!val && !isMarker && _outputDisabledCount == 0) || (isMarker && !markerHandler)) { + // Call delegate's error-reporting method, if implemented. + [self reportError:[NSString stringWithFormat:@"\"%@\" is not a valid %@", + matchMarker, (isMarker) ? @"marker" : @"variable"] + code:((isMarker) ? 2 : 3) continuing:YES]; + } + } else { + // Append output to end of template. + if (_outputDisabledCount == 0) { + [output appendFormat:@"%@", [templateContents substringWithRange:remainingRange]]; + } + + // Check to see if there are open blocks left over. + int openBlocks = [_openBlocksStack count]; + if (openBlocks > 0) { + NSString *errMsg = [NSString stringWithFormat:@"Finished processing template, but %d %@ left open (%@).", + openBlocks, + (openBlocks == 1) ? @"block was" : @"blocks were", + [[_openBlocksStack valueForKeyPath:BLOCK_NAME_KEY] componentsJoinedByString:@", "]]; + [self reportError:errMsg code:6 continuing:YES]; + } + + // Ensure we terminate the loop. + remainingRange.location = NSNotFound; + } + } + + // Tell all marker-handlers we're done. + [[_markers allValues] makeObjectsPerformSelector:@selector(engineFinishedProcessingTemplate)]; + + // Inform delegate we're done. + [self reportTemplateProcessingFinished]; + + return output; +} + + +- (NSString *)processTemplateInFileAtPath:(NSString *)templatePath withVariables:(NSDictionary *)variables +{ + NSString *result = nil; + NSStringEncoding enc; + NSString *templateString = [NSString stringWithContentsOfFile:templatePath usedEncoding:&enc error:NULL]; + if (templateString) { + result = [self processTemplate:templateString withVariables:variables]; + } + return result; +} + + +#pragma mark Properties + + +@synthesize markerStartDelimiter; +@synthesize markerEndDelimiter; +@synthesize expressionStartDelimiter; +@synthesize expressionEndDelimiter; +@synthesize filterDelimiter; +@synthesize literalStartMarker; +@synthesize literalEndMarker; +@synthesize remainingRange; +@synthesize delegate; +@synthesize matcher; +@synthesize templateContents; + + +@end diff --git a/Source/MGTemplateFilter.h b/Source/MGTemplateFilter.h new file mode 100644 index 00000000..44c23423 --- /dev/null +++ b/Source/MGTemplateFilter.h @@ -0,0 +1,14 @@ +/* + * MGTemplateFilter.h + * + * Created by Matt Gemmell on 12/05/2008. + * Copyright 2008 Instinctive Code. All rights reserved. + * + */ + +@protocol MGTemplateFilter + +- (NSArray *)filters; +- (NSObject *)filterInvoked:(NSString *)filter withArguments:(NSArray *)args onValue:(NSObject *)value; + +@end diff --git a/Source/MGTemplateMarker.h b/Source/MGTemplateMarker.h new file mode 100644 index 00000000..ccdec72d --- /dev/null +++ b/Source/MGTemplateMarker.h @@ -0,0 +1,41 @@ +/* + * MGTemplateMarker.h + * + * Created by Matt Gemmell on 12/05/2008. + * Copyright 2008 Instinctive Code. All rights reserved. + * + */ + +#import "MGTemplateEngine.h" + +@protocol MGTemplateMarker +@required +- (id)initWithTemplateEngine:(MGTemplateEngine *)engine; // to avoid retain cycles, use a weak reference for engine. +- (NSArray *)markers; // array of markers (each unique across all markers) this object handles. +- (NSArray *)endMarkersForMarker:(NSString *)marker; // returns the possible corresponding end-markers for a marker which has just started a block. +- (NSObject *)markerEncountered:(NSString *)marker withArguments:(NSArray *)args inRange:(NSRange)markerRange + blockStarted:(BOOL *)blockStarted blockEnded:(BOOL *)blockEnded + outputEnabled:(BOOL *)outputEnabled nextRange:(NSRange *)nextRange + currentBlockInfo:(NSDictionary *)blockInfo newVariables:(NSDictionary **)newVariables; +/* Notes for -markerEncountered:... method + Arguments: + marker: marker encountered by the template engine + args: arguments to the marker, in order + markerRange: the range of the marker encountered in the engine's templateString + blockStarted: pointer to BOOL. Set it to YES if the marker just started a block. + blockEnded: pointer to BOOL. Set it to YES if the marker just ended a block. + Note: you should never set both blockStarted and blockEnded in the same call. + outputEnabled: pointer to BOOL, indicating whether the engine is currently outputting. Can be changed to switch output on/off. + nextRange: the next range in the engine's templateString which will be searched. Can be modified if necessary. + currentBlockInfo: information about the current block, if the block was started by this handler; otherwise nil. + Note: if supplied, will include a dictionary of variables set for the current block. + newVariables: variables to set in the template context. If blockStarted is YES, these will be scoped only within the new block. + Note: if currentBlockInfo was specified, variables set in the return dictionary will override/update any variables of + the same name in currentBlockInfo's variables. This is for ease of updating loop-counters or such. + Returns: + A return value to insert into the template output, or nil if nothing should be inserted. + */ + +- (void)engineFinishedProcessingTemplate; + +@end diff --git a/Source/MGTemplateStandardFilters.h b/Source/MGTemplateStandardFilters.h new file mode 100644 index 00000000..335b0713 --- /dev/null +++ b/Source/MGTemplateStandardFilters.h @@ -0,0 +1,15 @@ +// +// MGTemplateStandardFilters.h +// +// Created by Matt Gemmell on 13/05/2008. +// Copyright 2008 Instinctive Code. All rights reserved. +// + +#import "MGTemplateFilter.h" + + +@interface MGTemplateStandardFilters : NSObject <MGTemplateFilter> { + +} + +@end diff --git a/Source/MGTemplateStandardFilters.m b/Source/MGTemplateStandardFilters.m new file mode 100644 index 00000000..45f1d51d --- /dev/null +++ b/Source/MGTemplateStandardFilters.m @@ -0,0 +1,97 @@ +// +// MGTemplateStandardFilters.m +// +// Created by Matt Gemmell on 13/05/2008. +// Copyright 2008 Instinctive Code. All rights reserved. +// + +#import "MGTemplateStandardFilters.h" + + +#define UPPERCASE @"uppercase" +#define LOWERCASE @"lowercase" +#define CAPITALIZED @"capitalized" +#define DATE_FORMAT @"date_format" +#define COLOR_FORMAT @"color_format" + + +@implementation MGTemplateStandardFilters + + +- (NSArray *)filters +{ + return [NSArray arrayWithObjects: + UPPERCASE, LOWERCASE, CAPITALIZED, + DATE_FORMAT, COLOR_FORMAT, + nil]; +} + + +- (NSObject *)filterInvoked:(NSString *)filter withArguments:(NSArray *)args onValue:(NSObject *)value +{ + if ([filter isEqualToString:UPPERCASE]) { + return [[NSString stringWithFormat:@"%@", value] uppercaseString]; + + } else if ([filter isEqualToString:LOWERCASE]) { + return [[NSString stringWithFormat:@"%@", value] lowercaseString]; + + } else if ([filter isEqualToString:CAPITALIZED]) { + return [[NSString stringWithFormat:@"%@", value] capitalizedString]; + + } else if ([filter isEqualToString:DATE_FORMAT]) { + // Formats NSDates according to Unicode syntax: + // http://unicode.org/reports/tr35/tr35-4.html#Date_Format_Patterns + // e.g. "dd MM yyyy" etc. + if ([value isKindOfClass:[NSDate class]] && [args count] == 1) { + NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease]; + [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; + NSString *format = [args objectAtIndex:0]; + [dateFormatter setDateFormat:format]; + return [dateFormatter stringFromDate:(NSDate *)value]; + } + + } else if ([filter isEqualToString:COLOR_FORMAT]) { +#if TARGET_OS_IPHONE + if ([value isKindOfClass:[UIColor class]] && [args count] == 1) { +#else + if ([value isKindOfClass:[NSColor class]] && [args count] == 1) { +#endif + NSString *format = [[args objectAtIndex:0] lowercaseString]; + if ([format isEqualToString:@"hex"]) { + // Output color in hex format RRGGBB (without leading # character). +#if TARGET_OS_IPHONE + CGColorRef color = [(UIColor *)value CGColor]; + CGColorSpaceRef colorSpace = CGColorGetColorSpace(color); + CGColorSpaceModel colorSpaceModel = CGColorSpaceGetModel(colorSpace); + + if (colorSpaceModel != kCGColorSpaceModelRGB) + return @"000000"; + + const CGFloat *components = CGColorGetComponents(color); + NSString *colorHex = [NSString stringWithFormat:@"%02x%02x%02x", + (int)(components[0] * 255), + (int)(components[1] * 255), + (int)(components[2] * 255)]; + return colorHex; +#else + NSColor *color = [(NSColor *)value colorUsingColorSpaceName:NSCalibratedRGBColorSpace]; + if (!color) { // happens if the colorspace couldn't be converted + return @"000000"; // black + } else { + NSString *colorHex = [NSString stringWithFormat:@"%02x%02x%02x", + (int)([color redComponent] * 255), + (int)([color greenComponent] * 255), + (int)([color blueComponent] * 255)]; + return colorHex; + } +#endif + } + } + + } + + return value; +} + + +@end diff --git a/Source/MGTemplateStandardMarkers.h b/Source/MGTemplateStandardMarkers.h new file mode 100644 index 00000000..2830197a --- /dev/null +++ b/Source/MGTemplateStandardMarkers.h @@ -0,0 +1,24 @@ +// +// MGTemplateStandardMarkers.h +// +// Created by Matt Gemmell on 13/05/2008. +// Copyright 2008 Instinctive Code. All rights reserved. +// + +#import "MGTemplateEngine.h" +#import "MGTemplateMarker.h" + +@interface MGTemplateStandardMarkers : NSObject <MGTemplateMarker> { + MGTemplateEngine *engine; // weak ref + NSMutableArray *forStack; + NSMutableArray *sectionStack; + NSMutableArray *ifStack; + NSMutableArray *commentStack; + NSMutableDictionary *cycles; +} + +- (BOOL)currentBlock:(NSDictionary *)blockInfo matchesTopOfStack:(NSMutableArray *)stack; +- (BOOL)argIsNumeric:(NSString *)arg intValue:(int *)val checkVariables:(BOOL)checkVars; +- (BOOL)argIsTrue:(NSString *)arg; + +@end diff --git a/Source/MGTemplateStandardMarkers.m b/Source/MGTemplateStandardMarkers.m new file mode 100644 index 00000000..df4fcf3e --- /dev/null +++ b/Source/MGTemplateStandardMarkers.m @@ -0,0 +1,620 @@ +// +// MGTemplateStandardMarkers.m +// +// Created by Matt Gemmell on 13/05/2008. +// Copyright 2008 Instinctive Code. All rights reserved. +// + +#import "MGTemplateStandardMarkers.h" +#import "MGTemplateFilter.h" + +//============================================================================== + +#define FOR_START @"for" +#define FOR_END @"/for" + +#define FOR_TYPE_ENUMERATOR @"in" // e.g. for thing in things +#define FOR_TYPE_RANGE @"to" // e.g. for 1 to 5 +#define FOR_REVERSE @"reversed" + +#define FOR_LOOP_VARS @"currentLoop" +#define FOR_LOOP_CURR_INDEX @"currentIndex" +#define FOR_LOOP_START_INDEX @"startIndex" +#define FOR_LOOP_END_INDEX @"endIndex" +#define FOR_PARENT_LOOP @"parentLoop" + +#define STACK_START_MARKER_RANGE @"markerRange" +#define STACK_START_REMAINING_RANGE @"remainingRange" +#define FOR_STACK_ENUMERATOR @"enumerator" +#define FOR_STACK_ENUM_VAR @"enumeratorVariable" +#define FOR_STACK_DISABLED_OUTPUT @"disabledOutput" + +//============================================================================== + +#define SECTION_START @"section" +#define SECTION_END @"/section" + +//============================================================================== + +#define IF_START @"if" +#define ELSE @"else" +#define IF_END @"/if" + +#define IF_VARS @"currentIf" +#define DISABLE_OUTPUT @"shouldDisableOutput" +#define IF_ARG_TRUE @"argumentTrue" +#define IF_ELSE_SEEN @"elseEncountered" + +//============================================================================== + +#define NOW @"now" + +//============================================================================== + +#define COMMENT_START @"comment" +#define COMMENT_END @"/comment" + +//============================================================================== + +#define LOAD @"load" + +//============================================================================== + +#define CYCLE @"cycle" +#define CYCLE_INDEX @"lastIndex" +#define CYCLE_VALUES @"value" + +//============================================================================== + +#define SET @"set" + +//============================================================================== + + +@implementation MGTemplateStandardMarkers + + +- (id)initWithTemplateEngine:(MGTemplateEngine *)theEngine +{ + if (self = [super init]) { + engine = theEngine; + forStack = [[NSMutableArray alloc] init]; + sectionStack = [[NSMutableArray alloc] init]; + ifStack = [[NSMutableArray alloc] init]; + commentStack = [[NSMutableArray alloc] init]; + cycles = [[NSMutableDictionary alloc] init]; + } + return self; +} + + +- (void)dealloc +{ + engine = nil; + [forStack release]; + forStack = nil; + [sectionStack release]; + sectionStack = nil; + [ifStack release]; + ifStack = nil; + [commentStack release]; + commentStack = nil; + [cycles release]; + cycles = nil; + + [super dealloc]; +} + + +- (NSArray *)markers +{ + return [NSArray arrayWithObjects: + FOR_START, FOR_END, + SECTION_START, SECTION_END, + IF_START, ELSE, IF_END, + NOW, + COMMENT_START, COMMENT_END, + LOAD, + CYCLE, + SET, + nil]; +} + + +- (NSArray *)endMarkersForMarker:(NSString *)marker +{ + if ([marker isEqualToString:FOR_START]) { + return [NSArray arrayWithObjects:FOR_END, nil]; + } else if ([marker isEqualToString:SECTION_START]) { + return [NSArray arrayWithObjects:SECTION_END, nil]; + } else if ([marker isEqualToString:IF_START]) { + return [NSArray arrayWithObjects:IF_END, ELSE, nil]; + } else if ([marker isEqualToString:COMMENT_START]) { + return [NSArray arrayWithObjects:COMMENT_END, nil]; + } + return nil; +} + + +- (NSObject *)markerEncountered:(NSString *)marker withArguments:(NSArray *)args inRange:(NSRange)markerRange + blockStarted:(BOOL *)blockStarted blockEnded:(BOOL *)blockEnded + outputEnabled:(BOOL *)outputEnabled nextRange:(NSRange *)nextRange + currentBlockInfo:(NSDictionary *)blockInfo newVariables:(NSDictionary **)newVariables +{ + if ([marker isEqualToString:FOR_START]) { + if (args && [args count] >= 3) { + // Determine which type of loop this is. + BOOL isRange = YES; + if ([[args objectAtIndex:1] isEqualToString:FOR_TYPE_ENUMERATOR]) { + isRange = NO; + } + BOOL reversed = NO; + if ([args count] == 4 && [[args objectAtIndex:3] isEqualToString:FOR_REVERSE]) { + reversed = YES; + } + + // Determine if we have acceptable parameters. + NSObject *loopEnumObject = nil; + BOOL valid = NO; + NSString *startArg = [args objectAtIndex:0]; + NSString *endArg = [args objectAtIndex:2]; + int startIndex, endIndex; + if (isRange) { + // Check to see if either the arg itself is numeric, or it corresponds to a numeric variable. + valid = [self argIsNumeric:startArg intValue:&startIndex checkVariables:YES]; + if (valid) { + valid = [self argIsNumeric:endArg intValue:&endIndex checkVariables:YES]; + if (valid) { + // Check startIndex and endIndex are sensible. + valid = (startIndex <= endIndex); + } + } + } else { + startIndex = 0; + + // Check that endArg is a collection. + NSObject *obj = [engine resolveVariable:endArg]; + if (obj && [obj respondsToSelector:@selector(objectEnumerator)] && [obj respondsToSelector:@selector(count)]) { + endIndex = [(NSArray *)obj count]; + if (endIndex > 0) { + loopEnumObject = obj; + valid = YES; + } + } + } + + if (valid) { + *blockStarted = YES; + + // Set up for-stack frame for this loop. + NSMutableDictionary *stackFrame = [NSMutableDictionary dictionaryWithObjectsAndKeys: + [NSValue valueWithRange:markerRange], STACK_START_MARKER_RANGE, + [NSValue valueWithRange:*nextRange], STACK_START_REMAINING_RANGE, + nil]; + [forStack addObject:stackFrame]; + + // Set up variables for the block. + int currentIndex = (reversed) ? endIndex : startIndex; + NSMutableDictionary *loopVars = [NSMutableDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithInt:startIndex], FOR_LOOP_START_INDEX, + [NSNumber numberWithInt:endIndex], FOR_LOOP_END_INDEX, + [NSNumber numberWithInt:currentIndex], FOR_LOOP_CURR_INDEX, + [NSNumber numberWithBool:reversed], FOR_REVERSE, + nil]; + NSMutableDictionary *blockVars = [NSMutableDictionary dictionaryWithObjectsAndKeys: + loopVars, FOR_LOOP_VARS, + nil]; + + // Add enumerator variable if appropriate. + if (!isRange) { + NSEnumerator *enumerator; + if (reversed && [loopEnumObject respondsToSelector:@selector(reverseObjectEnumerator)]) { + enumerator = [(NSArray *)loopEnumObject reverseObjectEnumerator]; + } else { + enumerator = [(NSArray *)loopEnumObject objectEnumerator]; + } + [stackFrame setObject:enumerator forKey:FOR_STACK_ENUMERATOR]; + [stackFrame setObject:startArg forKey:FOR_STACK_ENUM_VAR]; + [blockVars setObject:[enumerator nextObject] forKey:startArg]; + } + + // Add parentLoop if it exists. + if (blockInfo) { + NSDictionary *parentLoop; + parentLoop = (NSDictionary *)[engine resolveVariable:FOR_LOOP_VARS]; // in case parent loop isn't in the first parent stack-frame. + if (parentLoop) { + [loopVars setObject:parentLoop forKey:FOR_PARENT_LOOP]; + } + } + + *newVariables = blockVars; + } else { + // Disable output for this block. + *blockStarted = YES; + NSMutableDictionary *stackFrame = [NSMutableDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithBool:YES], FOR_STACK_DISABLED_OUTPUT, + [NSValue valueWithRange:markerRange], STACK_START_MARKER_RANGE, + [NSValue valueWithRange:*nextRange], STACK_START_REMAINING_RANGE, + nil]; + [forStack addObject:stackFrame]; + *outputEnabled = NO; + } + } + + } else if ([marker isEqualToString:FOR_END]) { + // Decide whether to loop back or terminate. + if ([self currentBlock:blockInfo matchesTopOfStack:forStack]) { + NSMutableDictionary *frame = [forStack lastObject]; + + // Check to see if this was a block with an invalid looping condition. + NSNumber *disabledOutput = (NSNumber *)[frame objectForKey:FOR_STACK_DISABLED_OUTPUT]; + if (disabledOutput && [disabledOutput boolValue]) { + *outputEnabled = YES; + *blockEnded = YES; + [forStack removeLastObject]; + } + + // This is the same loop that's on top of our stack. Check to see if we need to loop back. + BOOL loop = NO; + NSDictionary *blockVars = [blockInfo objectForKey:BLOCK_VARIABLES_KEY]; + if ([blockVars count] == 0) { + *blockEnded = YES; + return nil; + } + NSMutableDictionary *loopVars = [[[blockVars objectForKey:FOR_LOOP_VARS] mutableCopy] autorelease]; + BOOL reversed = [[loopVars objectForKey:FOR_REVERSE] boolValue]; + NSEnumerator *loopEnum = [frame objectForKey:FOR_STACK_ENUMERATOR]; + NSObject *newEnumValue = nil; + int currentIndex = [[loopVars objectForKey:FOR_LOOP_CURR_INDEX] intValue]; + if (loopEnum) { + // Enumerator type. + newEnumValue = [loopEnum nextObject]; + if (newEnumValue) { + loop = YES; + } + } else { + // Range type. + if (reversed) { + int minIndex = [[loopVars objectForKey:FOR_LOOP_START_INDEX] intValue]; + if (currentIndex > minIndex) { + loop = YES; + } + } else { + int maxIndex = [[loopVars objectForKey:FOR_LOOP_END_INDEX] intValue]; + if (currentIndex < maxIndex) { + loop = YES; + } + } + } + + if (loop) { + // Set remainingRange from stack dict + *nextRange = [[frame objectForKey:STACK_START_REMAINING_RANGE] rangeValue]; + + // Set new currentIndex + if (reversed) { + currentIndex--; + } else { + currentIndex++; + } + [loopVars setObject:[NSNumber numberWithInt:currentIndex] forKey:FOR_LOOP_CURR_INDEX]; + + // Set new val for enumVar if specified + NSMutableDictionary *newVars = [NSMutableDictionary dictionaryWithObjectsAndKeys: + loopVars, FOR_LOOP_VARS, + nil]; + if (newEnumValue) { + [newVars setObject:newEnumValue forKey:[frame objectForKey:FOR_STACK_ENUM_VAR]]; + } + + *newVariables = newVars; + } else { + // Don't need to do much here, since: + // 1. Each blockStack frame for a "for" has its own currentLoop dict. + // 2. Parent loop's enum-vars are still in place in the parent stack's vars. + + // End block. + *blockEnded = YES; + [forStack removeLastObject]; + } + + // Return immediately. + return nil; + } + *blockEnded = YES; + + } else if ([marker isEqualToString:SECTION_START]) { + if (args && [args count] == 1) { + *blockStarted = YES; + + // Set up for-stack frame for this section. + NSMutableDictionary *stackFrame = [NSMutableDictionary dictionaryWithObjectsAndKeys: + [NSValue valueWithRange:markerRange], STACK_START_MARKER_RANGE, + nil]; + [sectionStack addObject:stackFrame]; + } + + } else if ([marker isEqualToString:SECTION_END]) { + if ([self currentBlock:blockInfo matchesTopOfStack:sectionStack]) { + // This is the same section that's on top of our stack. Remove from stack. + [sectionStack removeLastObject]; + } + *blockEnded = YES; + + } else if ([marker isEqualToString:IF_START]) { + if (args && ([args count] == 1 || [args count] == 3)) { + *blockStarted = YES; + + // Determine appropriate values for outputEnabled and for our if-stack frame. + BOOL elseEncountered = NO; + BOOL argTrue = NO; + if ([args count] == 1) { + argTrue = [self argIsTrue:[args objectAtIndex:0]]; + } else if ([args count] == 2 && [[[args objectAtIndex:0] lowercaseString] isEqualToString:@"not"]) { + // e.g. if not x + argTrue = ![self argIsTrue:[args objectAtIndex:1]]; + } else if ([args count] == 3) { + // Assumed to be of the form: operand comparison operand, e.g. x == y + NSString *firstArg = [args objectAtIndex:0]; + NSString *secondArg = [args objectAtIndex:2]; + BOOL firstTrue = [self argIsTrue:firstArg]; + BOOL secondTrue = [self argIsTrue:secondArg]; + int num1, num2; + BOOL firstNumeric, secondNumeric; + firstNumeric = [self argIsNumeric:firstArg intValue:&num1 checkVariables:YES]; + secondNumeric = [self argIsNumeric:secondArg intValue:&num2 checkVariables:YES]; + if (!firstNumeric) { + num1 = ([engine resolveVariable:firstArg]) ? 1 : 0; + } + if (!secondNumeric) { + num2 = ([engine resolveVariable:secondArg]) ? 1 : 0; + } + NSString *op = [[args objectAtIndex:1] lowercaseString]; + + if ([op isEqualToString:@"and"] || [op isEqualToString:@"&&"]) { + argTrue = (firstTrue && secondTrue); + } else if ([op isEqualToString:@"or"] || [op isEqualToString:@"||"]) { + argTrue = (firstTrue || secondTrue); + } else if ([op isEqualToString:@"="] || [op isEqualToString:@"=="]) { + argTrue = (num1 == num2); + } else if ([op isEqualToString:@"!="] || [op isEqualToString:@"<>"]) { + argTrue = (num1 != num2); + } else if ([op isEqualToString:@">"]) { + argTrue = (num1 > num2); + } else if ([op isEqualToString:@"<"]) { + argTrue = (num1 < num2); + } else if ([op isEqualToString:@">="]) { + argTrue = (num1 >= num2); + } else if ([op isEqualToString:@"<="]) { + argTrue = (num1 <= num2); + } else if ([op isEqualToString:@"\%"]) { + argTrue = ((num1 % num2) > 0); + } + } + + BOOL shouldDisableOutput = *outputEnabled; + if (shouldDisableOutput && !argTrue) { + *outputEnabled = NO; + } + + // Create variables. + NSMutableDictionary *ifVars = [NSMutableDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithBool:argTrue], IF_ARG_TRUE, + [NSNumber numberWithBool:shouldDisableOutput], DISABLE_OUTPUT, + [NSNumber numberWithBool:elseEncountered], IF_ELSE_SEEN, + nil]; + + // Set up for-stack frame for this if-statement. + NSMutableDictionary *stackFrame = [NSMutableDictionary dictionaryWithObjectsAndKeys: + [NSValue valueWithRange:markerRange], STACK_START_MARKER_RANGE, + ifVars, IF_VARS, + nil]; + [ifStack addObject:stackFrame]; + } + + } else if ([marker isEqualToString:ELSE]) { + if ([self currentBlock:blockInfo matchesTopOfStack:ifStack]) { + NSMutableDictionary *frame = [[ifStack lastObject] objectForKey:IF_VARS]; + BOOL elseSeen = [[frame objectForKey:IF_ELSE_SEEN] boolValue]; + BOOL argTrue = [[frame objectForKey:IF_ARG_TRUE] boolValue]; + BOOL modifyOutput = [[frame objectForKey:DISABLE_OUTPUT] boolValue]; + + if (!elseSeen) { + if (modifyOutput) { + // Only make changes if we've not already seen an 'else' for this block, + // and if we're modifying output state at all. + *outputEnabled = !argTrue; // either turning it off, or turning it back on. + } + + // Note that we've now seen the else marker. + [frame setObject:[NSNumber numberWithBool:YES] forKey:IF_ELSE_SEEN]; + } + } + + } else if ([marker isEqualToString:IF_END]) { + if ([self currentBlock:blockInfo matchesTopOfStack:ifStack]) { + NSMutableDictionary *frame = [[ifStack lastObject] objectForKey:IF_VARS]; + BOOL modifyOutput = [[frame objectForKey:DISABLE_OUTPUT] boolValue]; + if (modifyOutput) { + // If we're modifying output, it was enabled when this block started. + // Thus, it should be enabled after the block ends. + // If it's already enabled, this will have no harmful effect. + *outputEnabled = YES; + } + + // End block. + [ifStack removeLastObject]; + *blockEnded = YES; + } + *blockEnded = YES; + + } else if ([marker isEqualToString:NOW]) { + return [NSDate date]; + + } else if ([marker isEqualToString:COMMENT_START]) { + // Work out if we need to start a block. + if (!args || [args count] == 0) { + *blockStarted = YES; + + // Determine appropriate values for outputEnabled and for our stack frame. + BOOL shouldDisableOutput = *outputEnabled; + if (shouldDisableOutput) { + *outputEnabled = NO; + } + + // Set up for-stack frame for this if-statement. + NSMutableDictionary *stackFrame = [NSMutableDictionary dictionaryWithObjectsAndKeys: + [NSValue valueWithRange:markerRange], STACK_START_MARKER_RANGE, + [NSNumber numberWithBool:shouldDisableOutput], DISABLE_OUTPUT, + nil]; + [commentStack addObject:stackFrame]; + } + + } else if ([marker isEqualToString:COMMENT_END]) { + // Check this is block on top of stack. + if ([self currentBlock:blockInfo matchesTopOfStack:commentStack]) { + NSMutableDictionary *frame = [commentStack lastObject]; + BOOL modifyOutput = [[frame objectForKey:DISABLE_OUTPUT] boolValue]; + if (modifyOutput) { + // If we're modifying output, it was enabled when this block started. + // Thus, it should be enabled after the block ends. + // If it's already enabled, this will have no harmful effect. + *outputEnabled = YES; + } + + // End block. + [commentStack removeLastObject]; + *blockEnded = YES; + } + *blockEnded = YES; + + } else if ([marker isEqualToString:LOAD]) { + if (args && [args count] > 0) { + for (NSString *className in args) { + Class class = NSClassFromString(className); + if (class && [(id)class isKindOfClass:[NSObject class]]) { + if ([class conformsToProtocol:@protocol(MGTemplateFilter)]) { + // Instantiate and load filter. + NSObject <MGTemplateFilter> *obj = [[[class alloc] init] autorelease]; + [engine loadFilter:obj]; + } else if ([class conformsToProtocol:@protocol(MGTemplateMarker)]) { + // Instantiate and load marker. + NSObject <MGTemplateMarker> *obj = [[[class alloc] initWithTemplateEngine:engine] autorelease]; + [engine loadMarker:obj]; + } + } + } + } + + } else if ([marker isEqualToString:CYCLE]) { + if (args && [args count] > 0) { + // Check to see if it's an existing cycle. + NSString *rangeKey = NSStringFromRange(markerRange); + NSMutableDictionary *cycle = [cycles objectForKey:rangeKey]; + if (cycle) { + NSArray *vals = [cycle objectForKey:CYCLE_VALUES]; + int currIndex = [[cycle objectForKey:CYCLE_INDEX] intValue]; + currIndex++; + if (currIndex >= [vals count]) { + currIndex = 0; + } + [cycle setObject:[NSNumber numberWithInt:currIndex] forKey:CYCLE_INDEX]; + return [vals objectAtIndex:currIndex]; + } else { + // New cycle. Create and output appropriately. + cycle = [NSMutableDictionary dictionaryWithCapacity:2]; + [cycle setObject:[NSNumber numberWithInt:0] forKey:CYCLE_INDEX]; + [cycle setObject:args forKey:CYCLE_VALUES]; + [cycles setObject:cycle forKey:rangeKey]; + return [args objectAtIndex:0]; + } + } + } else if ([marker isEqualToString:SET]) { + if (args && [args count] == 2 && *outputEnabled) { + // Set variable arg1 to value arg2. + NSDictionary *newVar = [NSDictionary dictionaryWithObject:[args objectAtIndex:1] + forKey:[args objectAtIndex:0]]; + if (newVar) { + *newVariables = newVar; + } + } + } + + return nil; +} + + +- (BOOL)currentBlock:(NSDictionary *)blockInfo matchesTopOfStack:(NSMutableArray *)stack +{ + if (blockInfo && [stack count] > 0) { // end-tag should always have blockInfo, and correspond to a stack frame. + NSDictionary *frame = [stack lastObject]; + NSRange stackSectionRange = [[frame objectForKey:STACK_START_MARKER_RANGE] rangeValue]; + NSRange thisSectionRange = [[blockInfo objectForKey:BLOCK_START_MARKER_RANGE_KEY] rangeValue]; + if (NSEqualRanges(stackSectionRange, thisSectionRange)) { + return YES; + } + } + return NO; +} + + +- (BOOL)argIsTrue:(NSString *)arg +{ + BOOL argTrue = NO; + if (arg) { + NSObject *val = [engine resolveVariable:arg]; + if (val) { + if ([val isKindOfClass:[NSNumber class]]) { + argTrue = [(NSNumber *)val boolValue]; + } else { + argTrue = YES; + } + } + } + return argTrue; +} + + +- (BOOL)argIsNumeric:(NSString *)arg intValue:(int *)val checkVariables:(BOOL)checkVars +{ + BOOL numeric = NO; + int value = 0; + + if (arg && [arg length] > 0) { + if ([[arg substringToIndex:1] isEqualToString:@"0"] || [arg intValue] != 0) { + numeric = YES; + value = [arg intValue]; + } else if (checkVars) { + // Check to see if arg is a variable with an intValue. + NSObject *argObj = [engine resolveVariable:arg]; + NSString *argStr = [NSString stringWithFormat:@"%@", argObj]; + if (argObj && [argObj respondsToSelector:@selector(intValue)] && + [self argIsNumeric:argStr intValue:&value checkVariables:NO]) { // avoid recursion + numeric = YES; + } + } + } + + if (val) { + *val = value; + } + return numeric; +} + + +- (void)engineFinishedProcessingTemplate +{ + // Clean up stacks etc. + [forStack release]; + forStack = [[NSMutableArray alloc] init]; + [sectionStack release]; + sectionStack = [[NSMutableArray alloc] init]; + [ifStack release]; + ifStack = [[NSMutableArray alloc] init]; + [commentStack release]; + commentStack = [[NSMutableArray alloc] init]; + cycles = [[NSMutableDictionary alloc] init]; +} + + +@end diff --git a/Source/NSArray_DeepMutableCopy.h b/Source/NSArray_DeepMutableCopy.h new file mode 100644 index 00000000..9f619d03 --- /dev/null +++ b/Source/NSArray_DeepMutableCopy.h @@ -0,0 +1,12 @@ +// +// NSArray_DeepMutableCopy.h +// +// Created by Matt Gemmell on 02/05/2008. +// Copyright 2008 Instinctive Code. All rights reserved. +// + +@interface NSArray (DeepMutableCopy) + +- (NSMutableArray *)deepMutableCopy; + +@end diff --git a/Source/NSArray_DeepMutableCopy.m b/Source/NSArray_DeepMutableCopy.m new file mode 100644 index 00000000..72f64c06 --- /dev/null +++ b/Source/NSArray_DeepMutableCopy.m @@ -0,0 +1,42 @@ +// +// NSArray_DeepMutableCopy.m +// +// Created by Matt Gemmell on 02/05/2008. +// Copyright 2008 Instinctive Code. All rights reserved. +// + +#import "NSArray_DeepMutableCopy.h" + + +@implementation NSArray (DeepMutableCopy) + + +- (NSMutableArray *)deepMutableCopy; +{ + NSMutableArray *newArray; + unsigned int index, count; + + count = [self count]; + newArray = [[NSMutableArray allocWithZone:[self zone]] initWithCapacity:count]; + for (index = 0; index < count; index++) { + id anObject; + + anObject = [self objectAtIndex:index]; + if ([anObject respondsToSelector:@selector(deepMutableCopy)]) { + anObject = [anObject deepMutableCopy]; + [newArray addObject:anObject]; + [anObject release]; + } else if ([anObject respondsToSelector:@selector(mutableCopyWithZone:)]) { + anObject = [anObject mutableCopyWithZone:nil]; + [newArray addObject:anObject]; + [anObject release]; + } else { + [newArray addObject:anObject]; + } + } + + return newArray; +} + + +@end diff --git a/Source/NSDictionary_DeepMutableCopy.h b/Source/NSDictionary_DeepMutableCopy.h new file mode 100644 index 00000000..7f94acde --- /dev/null +++ b/Source/NSDictionary_DeepMutableCopy.h @@ -0,0 +1,12 @@ +// +// NSDictionary_DeepMutableCopy.h +// +// Created by Matt Gemmell on 02/05/2008. +// Copyright 2008 Instinctive Code. All rights reserved. +// + +@interface NSDictionary (DeepMutableCopy) + +- (NSMutableDictionary *)deepMutableCopy; + +@end diff --git a/Source/NSDictionary_DeepMutableCopy.m b/Source/NSDictionary_DeepMutableCopy.m new file mode 100644 index 00000000..a5a49333 --- /dev/null +++ b/Source/NSDictionary_DeepMutableCopy.m @@ -0,0 +1,43 @@ +// +// NSDictionary_DeepMutableCopy.m +// +// Created by Matt Gemmell on 02/05/2008. +// Copyright 2008 Instinctive Code. All rights reserved. +// + +#import "NSDictionary_DeepMutableCopy.h" + + +@implementation NSDictionary (DeepMutableCopy) + + +- (NSMutableDictionary *)deepMutableCopy; +{ + NSMutableDictionary *newDictionary; + NSEnumerator *keyEnumerator; + id anObject; + id aKey; + + newDictionary = [self mutableCopy]; + // Run through the new dictionary and replace any objects that respond to -deepMutableCopy or -mutableCopy with copies. + keyEnumerator = [[newDictionary allKeys] objectEnumerator]; + while ((aKey = [keyEnumerator nextObject])) { + anObject = [newDictionary objectForKey:aKey]; + if ([anObject respondsToSelector:@selector(deepMutableCopy)]) { + anObject = [anObject deepMutableCopy]; + [newDictionary setObject:anObject forKey:aKey]; + [anObject release]; + } else if ([anObject respondsToSelector:@selector(mutableCopyWithZone:)]) { + anObject = [anObject mutableCopyWithZone:nil]; + [newDictionary setObject:anObject forKey:aKey]; + [anObject release]; + } else { + [newDictionary setObject:anObject forKey:aKey]; + } + } + + return newDictionary; +} + + +@end diff --git a/Source/RegexKitLite.h b/Source/RegexKitLite.h new file mode 100644 index 00000000..0338f582 --- /dev/null +++ b/Source/RegexKitLite.h @@ -0,0 +1,130 @@ +// +// RegexKitLite.h +// http://regexkit.sourceforge.net/ +// Licensesd under the terms of the BSD License, as specified below. +// + +/* + Copyright (c) 2008, John Engelhart + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of the Zang Industries nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef __OBJC__ + +#import <Foundation/NSObjCRuntime.h> +#import <Foundation/NSRange.h> +#import <Foundation/NSString.h> + +#endif // __OBJC__ + +#include <limits.h> +#include <sys/types.h> + +#ifdef __cplusplus +extern "C" { +#endif + +// For Mac OS X < 10.5. +#ifndef NSINTEGER_DEFINED +#define NSINTEGER_DEFINED +#ifdef __LP64__ || NS_BUILD_32_LIKE_64 +typedef long NSInteger; +typedef unsigned long NSUInteger; +#define NSIntegerMin LONG_MIN +#define NSIntegerMax LONG_MAX +#define NSUIntegerMax ULONG_MAX +#else +typedef int NSInteger; +typedef unsigned int NSUInteger; +#define NSIntegerMin INT_MIN +#define NSIntegerMax INT_MAX +#define NSUIntegerMax UINT_MAX +#endif +#endif // NSINTEGER_DEFINED + +#ifndef _REGEXKITLITE_H_ +#define _REGEXKITLITE_H_ + +#ifdef __OBJC__ + +@class NSError; + +// NSError error domains and user info keys. +extern NSString * const RKLICURegexErrorDomain; + +extern NSString * const RKLICURegexErrorNameErrorKey; +extern NSString * const RKLICURegexLineErrorKey; +extern NSString * const RKLICURegexOffsetErrorKey; +extern NSString * const RKLICURegexPreContextErrorKey; +extern NSString * const RKLICURegexPostContextErrorKey; +extern NSString * const RKLICURegexRegexErrorKey; +extern NSString * const RKLICURegexRegexOptionsErrorKey; + +// These must be idential to their ICU regex counterparts. See http://www.icu-project.org/userguide/regexp.html +enum { + RKLNoOptions = 0, + RKLCaseless = 2, + RKLComments = 4, + RKLDotAll = 32, + RKLMultiline = 8, + RKLUnicodeWordBoundaries = 256 +}; +typedef uint32_t RKLRegexOptions; + +@interface NSString (RegexKitLiteAdditions) + ++ (void)clearStringCache; + ++ (NSInteger)captureCountForRegex:(NSString *)regexString; ++ (NSInteger)captureCountForRegex:(NSString *)regexString options:(RKLRegexOptions)options error:(NSError **)error; + +- (BOOL)isMatchedByRegex:(NSString *)regexString; +- (BOOL)isMatchedByRegex:(NSString *)regexString options:(RKLRegexOptions)options inRange:(NSRange)range error:(NSError **)error; + +- (NSRange)rangeOfRegex:(NSString *)regexString; +- (NSRange)rangeOfRegex:(NSString *)regexString capture:(NSInteger)capture; +- (NSRange)rangeOfRegex:(NSString *)regexString inRange:(NSRange)range; +- (NSRange)rangeOfRegex:(NSString *)regexString options:(RKLRegexOptions)options inRange:(NSRange)range capture:(NSInteger)capture error:(NSError **)error; + +- (NSString *)stringByMatching:(NSString *)regexString; +- (NSString *)stringByMatching:(NSString *)regexString capture:(NSInteger)capture; +- (NSString *)stringByMatching:(NSString *)regexString inRange:(NSRange)range; +- (NSString *)stringByMatching:(NSString *)regexString options:(RKLRegexOptions)options inRange:(NSRange)range capture:(NSInteger)capture error:(NSError **)error; + +@end + +#endif // _REGEXKITLITE_H_ + +#endif // __OBJC__ + +#ifdef __cplusplus +} // extern "C" +#endif + diff --git a/Source/RegexKitLite.m b/Source/RegexKitLite.m new file mode 100644 index 00000000..9e77bd28 --- /dev/null +++ b/Source/RegexKitLite.m @@ -0,0 +1,354 @@ +// +// RegexKitLite.m +// http://regexkit.sourceforge.net/ +// Licensesd under the terms of the BSD License, as specified below. +// + +/* + Copyright (c) 2008, John Engelhart + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of the Zang Industries nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#import <CoreFoundation/CFBase.h> +#import <CoreFoundation/CFString.h> +#import <Foundation/NSDictionary.h> +#import <Foundation/NSError.h> +#import <Foundation/NSException.h> +#import <libkern/OSAtomic.h> +#import <string.h> +#import <stdlib.h> +#import "RegexKitLite.h" + +#ifndef RKL_CACHE_SIZE +#define RKL_CACHE_SIZE 23 +#endif + +#ifndef RKL_FIXED_LENGTH +#define RKL_FIXED_LENGTH 2048 +#endif + +// Ugly macros to keep other parts clean. + +#define NSRangeInsideRange(inside, within) ({NSRange _inside = (inside), _within = (within); (((_inside.location - _within.location) <= _within.length) && ((NSMaxRange(_inside) - _within.location) <= _within.length));}) +#define NSEqualRanges(range1, range2) ({NSRange _r1 = (range1), _r2 = (range2); ((_r1.location == _r2.location) && (_r1.length == _r2.length));}) +#define NSMakeRange(loc, len) ((NSRange){(NSUInteger)(loc), (NSUInteger)(len)}) +#define CFMakeRange(loc, len) ((CFRange){(CFIndex)(loc), (CFIndex)(len)}) +#define NSMaxRange(r) ({NSRange _r = (r); _r.location + _r.length;}) +#define NSNotFoundRange ((NSRange){NSNotFound, 0}) +#define NSMaxiumRange ((NSRange){0, NSUIntegerMax}) + +#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4 +#define CFAutorelease(obj) ({CFTypeRef _obj = (obj); (_obj == NULL) ? NULL : [(id)CFMakeCollectable(_obj) autorelease]; }) +#else +#define CFAutorelease(obj) ({CFTypeRef _obj = (obj); (_obj == NULL) ? NULL : [(id)(_obj) autorelease]; }) +#endif + +#define RKLMakeString(str, hash, len, uc) ((RKLString){(str), (hash), (len), (UniChar *)(uc)}) +#define RKLClearCacheSlotLastString(ce) ({ ce->last = RKLMakeString(NULL, 0, 0, NULL); ce->lastFindRange = NSNotFoundRange; ce->lastMatchRange = NSNotFoundRange; }) +#define RKLGetRangeForCapture(regex, status, capture, range) ({ range.location = (NSUInteger)uregex_start(regex, capture, &status); range.length = (NSUInteger)uregex_end(regex, capture, &status) - range.location; status; }) +#define RKLInternalException [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"An internal error occured at %@:%d", [NSString stringWithUTF8String:__FILE__], __LINE__] userInfo:NULL] + +// Exported symbols. Error domains, keys, etc. +NSString * const RKLICURegexErrorDomain = @"RKLICURegexErrorDomain"; + +NSString * const RKLICURegexErrorNameErrorKey = @"RKLICURegexErrorName"; +NSString * const RKLICURegexLineErrorKey = @"RKLICURegexLine"; +NSString * const RKLICURegexOffsetErrorKey = @"RKLICURegexOffset"; +NSString * const RKLICURegexPreContextErrorKey = @"RKLICURegexPreContext"; +NSString * const RKLICURegexPostContextErrorKey = @"RKLICURegexPostContext"; +NSString * const RKLICURegexRegexErrorKey = @"RKLICURegexRegex"; +NSString * const RKLICURegexRegexOptionsErrorKey = @"RKLICURegexRegexOptions"; + +// Type / struct definitions + +typedef struct uregex uregex; // Opaque ICU regex type. + +#define U_PARSE_CONTEXT_LEN 16 + +typedef struct UParseError { + int32_t line; + int32_t offset; + unichar preContext[U_PARSE_CONTEXT_LEN]; + unichar postContext[U_PARSE_CONTEXT_LEN]; +} UParseError; + +typedef struct { + void *string; // Used ONLY for pointer equality tests! Never messaged! + CFHashCode hash; + NSUInteger length; + UniChar *uniChar; +} RKLString; + +typedef struct { + NSString *regexString; + RKLRegexOptions regexOptions; + uregex *icu_regex; + NSInteger captureCount; + + RKLString last; + NSRange lastFindRange; + NSRange lastMatchRange; +} RKLCacheSlot; + +// ICU functions. See http://www.icu-project.org/apiref/icu4c/uregex_8h.html Tweaked slightly from the originals, but functionally identical. +const char * u_errorName (int32_t status); +int32_t u_strlen (const UniChar *s); +void uregex_close (uregex *regexp); +int32_t uregex_end (uregex *regexp, int32_t groupNum, int32_t *status); +BOOL uregex_find (uregex *regexp, int32_t location, int32_t *status); +BOOL uregex_findNext (uregex *regexp, int32_t *status); +int32_t uregex_groupCount (uregex *regexp, int32_t *status); +uregex * uregex_open (const UniChar *pattern, int32_t patternLength, RKLRegexOptions flags, UParseError *parseError, int32_t *status); +void uregex_setText (uregex *regexp, const UniChar *text, int32_t textLength, int32_t *status); +int32_t uregex_start (uregex *regexp, int32_t groupNum, int32_t *status); + +static RKLCacheSlot *getCachedRegex (NSString *regexString, RKLRegexOptions regexOptions, NSError **error); +static NSError *RKLNSErrorForRegex (NSString *regexString, RKLRegexOptions regexOptions, UParseError *parseError, int status); + +// Compile unit local global variables +static OSSpinLock cacheSpinLock = OS_SPINLOCK_INIT; +static RKLCacheSlot RKLCache[RKL_CACHE_SIZE]; +static RKLCacheSlot *lastCacheSlot; +static void *lastRegexString; +static UniChar fixedUniChar[(RKL_FIXED_LENGTH * sizeof(UniChar))]; +static RKLString fixedString = {NULL, 0, 0, &fixedUniChar[0]}; +static RKLString dynamicString; + +// IMPORTANT! This code is critical path code. Because of this, it has been written for speed, not clarity. +// IMPORTANT! Should only be called with cacheSpinLock already locked! +// ---------- + +static RKLCacheSlot *getCachedRegex(NSString *regexString, RKLRegexOptions regexOptions, NSError **error) { + CFHashCode regexHash = CFHash(regexString); + RKLCacheSlot *cacheSlot = &RKLCache[regexHash % RKL_CACHE_SIZE]; // Retrieve the cache slot for this regex. + UParseError parseError = (UParseError){-1, -1, {0}, {0}}; + UniChar *regexUniChar = NULL; + CFIndex regexLength = 0; + int32_t status = 0; + + // Return the cached entry if it's a match, otherwise clear the slot and create a new ICU regex in its place. + if((cacheSlot->regexOptions == regexOptions) && (cacheSlot->icu_regex != NULL) && (cacheSlot->regexString != NULL) && (CFEqual(regexString, cacheSlot->regexString) == YES)) { lastCacheSlot = cacheSlot; lastRegexString = regexString; return(cacheSlot); } + + RKLClearCacheSlotLastString(cacheSlot); // Clear any cached string state for this cache slot. + if(cacheSlot->regexString != NULL) { CFRelease(cacheSlot->regexString); cacheSlot->regexString = NULL; cacheSlot->regexOptions = 0; } + if(cacheSlot->icu_regex != NULL) { uregex_close(cacheSlot->icu_regex); cacheSlot->icu_regex = NULL; cacheSlot->captureCount = -1; } + + cacheSlot->regexString = (NSString *)CFStringCreateCopy(NULL, (CFStringRef)regexString); // Get a cheap immutable copy. + cacheSlot->regexOptions = regexOptions; + regexLength = CFStringGetLength((CFStringRef)regexString); // In UTF16 code pairs. + + // Try to quickly obtain the regex string in UTF16 format. Otherwise allocate enough space on the stack and convert to UTF16 using the stack buffer. + if((regexUniChar = (UniChar *)CFStringGetCharactersPtr((CFStringRef)regexString)) == NULL) { + if((regexUniChar = alloca(regexLength * sizeof(UniChar))) == NULL) { return(NULL); } + CFStringGetCharacters((CFStringRef)regexString, CFRangeMake(0, regexLength), regexUniChar); + } + + // Create the ICU regex. If there is a problem, create a NSError if requested. + if(((cacheSlot->icu_regex = uregex_open(regexUniChar, (int32_t)regexLength, regexOptions, &parseError, &status)) == NULL) && (status > 0)) { + if(error != NULL) { *error = RKLNSErrorForRegex(regexString, regexOptions, &parseError, status); } + return(NULL); + } + + cacheSlot->captureCount = (NSUInteger)uregex_groupCount(cacheSlot->icu_regex, &status); + lastCacheSlot = cacheSlot; + lastRegexString = regexString; + + return(cacheSlot); +} + +static NSError *RKLNSErrorForRegex(NSString *regexString, RKLRegexOptions regexOptions, UParseError *parseError, int status) { + NSNumber *regexOptionsNumber = [NSNumber numberWithInt:regexOptions]; + NSNumber *lineNumber = [NSNumber numberWithInt:parseError->line]; + NSNumber *offsetNumber = [NSNumber numberWithInt:parseError->offset]; + NSString *preContextString = [NSString stringWithCharacters:&parseError->preContext[0] length:u_strlen(&parseError->preContext[0])]; + NSString *postContextString = [NSString stringWithCharacters:&parseError->postContext[0] length:u_strlen(&parseError->postContext[0])]; + NSString *errorNameString = [NSString stringWithUTF8String:u_errorName(status)]; + NSString *reasonString = [NSString stringWithFormat:@"The error %@ occured at line %d, column %d: %@<<HERE>>%@", errorNameString, parseError->line, parseError->offset, preContextString, postContextString]; + + // If line == -1, parseError doesn't contain any useful information. Set lineNumber to NULL, + // which will stop adding objects to the dictionary at that point, ignoring everything after. + if(parseError->line == -1) { reasonString = [NSString stringWithFormat:@"The error %@ occured.", errorNameString]; lineNumber = NULL; } + + return([NSError errorWithDomain:RKLICURegexErrorDomain code:(NSInteger)status userInfo:[NSDictionary dictionaryWithObjectsAndKeys: @"There was an error compiling the regular expression.", @"NSLocalizedDescription", reasonString, @"NSLocalizedFailureReason", regexString, RKLICURegexRegexErrorKey, regexOptionsNumber, RKLICURegexRegexOptionsErrorKey, lineNumber, RKLICURegexLineErrorKey, offsetNumber, RKLICURegexOffsetErrorKey, preContextString, RKLICURegexPreContextErrorKey, postContextString, RKLICURegexPostContextErrorKey, errorNameString, RKLICURegexErrorNameErrorKey, NULL]]); +} + +@implementation NSString (RegexKitLiteAdditions) + ++ (void)clearStringCache +{ + OSSpinLockLock(&cacheSpinLock); + fixedString = RKLMakeString(NULL, 0, 0, fixedString.uniChar); + dynamicString = RKLMakeString(NULL, 0, 0, reallocf(dynamicString.uniChar, 0)); + NSUInteger x = 0; + for(x = 0; x < RKL_CACHE_SIZE; x++) { RKLClearCacheSlotLastString((&RKLCache[x])); } + OSSpinLockUnlock(&cacheSpinLock); +} + ++ (NSInteger)captureCountForRegex:(NSString *)regexString +{ + return([self captureCountForRegex:regexString options:RKLNoOptions error:NULL]); +} + ++ (NSInteger)captureCountForRegex:(NSString *)regexString options:(RKLRegexOptions)options error:(NSError **)error +{ + if(error != NULL) { *error = NULL; } + if(regexString == NULL) { [NSException raise:NSInvalidArgumentException format:@"The regular expression argument is NULL."]; } + + RKLCacheSlot *cacheSlot = NULL; + NSInteger captureCount = -1; + + OSSpinLockLock(&cacheSpinLock); + if((cacheSlot = getCachedRegex(regexString, options, error)) != NULL) { captureCount = cacheSlot->captureCount; } + OSSpinLockUnlock(&cacheSpinLock); + + return(captureCount); +} + +- (BOOL)isMatchedByRegex:(NSString *)regexString +{ + return([self isMatchedByRegex:regexString options:RKLNoOptions inRange:NSMaxiumRange error:NULL]); +} + +- (BOOL)isMatchedByRegex:(NSString *)regexString options:(RKLRegexOptions)options inRange:(NSRange)range error:(NSError **)error +{ + return(([self rangeOfRegex:regexString options:options inRange:range capture:0 error:error].location == NSNotFound) ? NO : YES); +} + +- (NSString *)stringByMatching:(NSString *)regexString +{ + return([self stringByMatching:regexString options:RKLNoOptions inRange:NSMaxiumRange capture:0 error:NULL]); +} + +- (NSString *)stringByMatching:(NSString *)regexString capture:(NSInteger)capture +{ + return([self stringByMatching:regexString options:RKLNoOptions inRange:NSMaxiumRange capture:capture error:NULL]); +} + +- (NSString *)stringByMatching:(NSString *)regexString inRange:(NSRange)range +{ + return([self stringByMatching:regexString options:RKLNoOptions inRange:range capture:0 error:NULL]); +} + +- (NSString *)stringByMatching:(NSString *)regexString options:(RKLRegexOptions)options inRange:(NSRange)range capture:(NSInteger)capture error:(NSError **)error +{ + NSRange matchedRange = [self rangeOfRegex:regexString options:options inRange:range capture:capture error:error]; + return((matchedRange.location == NSNotFound) ? NULL : CFAutorelease(CFStringCreateWithSubstring(NULL, (CFStringRef)self, CFMakeRange(matchedRange.location, matchedRange.length)))); +} + +- (NSRange)rangeOfRegex:(NSString *)regexString +{ + return([self rangeOfRegex:regexString options:RKLNoOptions inRange:NSMaxiumRange capture:0 error:NULL]); +} + +- (NSRange)rangeOfRegex:(NSString *)regexString capture:(NSInteger)capture +{ + return([self rangeOfRegex:regexString options:RKLNoOptions inRange:NSMaxiumRange capture:capture error:NULL]); +} + +- (NSRange)rangeOfRegex:(NSString *)regexString inRange:(NSRange)range +{ + return([self rangeOfRegex:regexString options:RKLNoOptions inRange:range capture:0 error:NULL]); +} + + +// IMPORTANT! This code is critical path code. Because of this, it has been written for speed, not clarity. +// ---------- + +- (NSRange)rangeOfRegex:(NSString *)regexString options:(RKLRegexOptions)options inRange:(NSRange)range capture:(NSInteger)capture error:(NSError **)error +{ + if(error != NULL) { *error = NULL; } + if(regexString == NULL) { [NSException raise:NSInvalidArgumentException format:@"The regular expression argument is NULL."]; } + + NSRange captureRange = NSNotFoundRange; + CFIndex stringLength = CFStringGetLength((CFStringRef)self); // In UTF16 code pairs. + RKLCacheSlot *cacheSlot = NULL; + NSException *exception = NULL; + int32_t status = 0; + + if(range.length == NSUIntegerMax) { range.length = stringLength; } // For convenience. + if((NSUInteger)stringLength < NSMaxRange(range)) { [NSException raise:NSRangeException format:@"The search range exceeds the strings bounds."]; } + + // IMPORTANT! Once we have obtained the lock, code MUST exit via 'goto exitNow;' to unlock the lock! NO EXCEPTIONS! + + OSSpinLockLock(&cacheSpinLock); // Grab the lock and get cache entry. + // Fast path the common case where this regex is the same one used last time. + // On a miss, do full lookup with getCachedRegex(), which compiles the regex if it's not in the cache. + if((lastCacheSlot != NULL) && (options == lastCacheSlot->regexOptions) && (CFEqual(regexString, lastCacheSlot->regexString) == YES)) { cacheSlot = lastCacheSlot; } + else if((cacheSlot = getCachedRegex(regexString, options, error)) == NULL) { goto exitNow; } + if(cacheSlot->icu_regex == NULL) { exception = RKLInternalException; goto exitNow; } // assertion check. + + if((capture < 0) || (capture > cacheSlot->captureCount)) { exception = [NSException exceptionWithName:NSInvalidArgumentException reason:@"The capture argument is not valid." userInfo:NULL]; goto exitNow; } + + RKLString selfString = RKLMakeString(self, CFHash(self), stringLength, CFStringGetCharactersPtr((CFStringRef)self)); + // *string will point to the most approrpiate buffer. If selfString contains a valid uniChar pointer, that's used. + // Otherwise, use the strings length to determine if the fixed or dynamically sized conversion buffer should be used. + RKLString *string = (selfString.uniChar != NULL) ? &selfString : (stringLength < RKL_FIXED_LENGTH) ? &fixedString : &dynamicString; + + // Check if this regex is already set to this string. + if((cacheSlot->last.uniChar == string->uniChar) && (cacheSlot->last.string == selfString.string) && (cacheSlot->last.hash == selfString.hash) && (cacheSlot->last.length == selfString.length) && (cacheSlot->last.string != NULL)) { goto alreadySetText; } + + // If we didn't get direct UTF16 access, perform any required UTF16 conversions if the current buffer doesn't match this string. + if((string != &selfString) && ((string->string != self) || (string->length != selfString.length) || (string->hash != selfString.hash))) { + *string = RKLMakeString(self, selfString.hash, selfString.length, string->uniChar); + // If this is the dynamically sized buffer, resize the allocation to the correct size. + if((stringLength >= RKL_FIXED_LENGTH) && ((string->uniChar = reallocf(string->uniChar, (selfString.length * sizeof(UniChar)))) == NULL)) { goto exitNow; } + CFStringGetCharacters((CFStringRef)self, CFRangeMake(0, string->length), string->uniChar); // Convert to a UTF16 string. + } + + RKLClearCacheSlotLastString(cacheSlot); // Clear the cached state for this regex. + if(string->uniChar == NULL) { exception = RKLInternalException; goto exitNow; } // assertion check. + uregex_setText(cacheSlot->icu_regex, string->uniChar, string->length, &status); // "set" the ICU regex to this string. + if(status != 0) { goto exitNow; } + cacheSlot->last = *string; // Cache the last string we set this regex to. + + alreadySetText: + if((NSEqualRanges(range, cacheSlot->lastFindRange) == NO)) { // Perform a 'find' if the current range is different than the last find range. + // Using uregex_findNext can be a slight performance win. + BOOL useFindNext = (range.location == (NSMaxRange(cacheSlot->lastMatchRange) + ((cacheSlot->lastMatchRange.length == 0) ? 1 : 0))) ? YES : NO; + + cacheSlot->lastFindRange = NSNotFoundRange; // Cleared the cached search/find range. + if(useFindNext == NO) { if((uregex_find (cacheSlot->icu_regex, range.location, &status) == NO) || (status != 0)) { goto exitNow; } } + else { if((uregex_findNext(cacheSlot->icu_regex, &status) == NO) || (status != 0)) { goto exitNow; } } + + if(RKLGetRangeForCapture(cacheSlot->icu_regex, status, 0, cacheSlot->lastMatchRange) != 0) { goto exitNow; } + cacheSlot->lastFindRange = range; // Cache the successful search/find range. + } + + if(NSRangeInsideRange(cacheSlot->lastMatchRange, range) == NO) { goto exitNow; } // If the regex matched outside the requested range, exit. + if(capture == 0) { captureRange = cacheSlot->lastMatchRange; } else { RKLGetRangeForCapture(cacheSlot->icu_regex, status, capture, captureRange); } + + exitNow: // A bit of advice... + OSSpinLockUnlock(&cacheSpinLock); // Always... no, no... never... forget to unlock your locks. + if(exception != NULL) { [exception raise]; } // I think the young people enjoy it when I "get down" verbally, don't you? + if(status > 0) { [NSException raise:NSInternalInconsistencyException format:@"ICU regular expression error #%d, %s", status, u_errorName(status)]; } + return((status == 0) ? captureRange : NSNotFoundRange); +} + +@end diff --git a/Source/TableContent.h b/Source/TableContent.h index cc242aba..6fa157a1 100644 --- a/Source/TableContent.h +++ b/Source/TableContent.h @@ -58,7 +58,7 @@ CMMCPConnection *mySQLConnection; id editData; - NSString *selectedTable; + NSString *selectedTable, *usedQuery; NSMutableArray *fullResult, *filteredResult, *keys; NSMutableDictionary *oldRow; NSString *compareType, *sortField; @@ -75,6 +75,8 @@ - (IBAction)filterTable:(id)sender; - (IBAction)showAll:(id)sender; - (IBAction)toggleFilterField:(id)sender; +- (NSString *)usedQuery; +- (void)setUsedQuery:(NSString *)query; //edit methods - (IBAction)addRow:(id)sender; diff --git a/Source/TableContent.m b/Source/TableContent.m index dfe9b24c..2a359d07 100644 --- a/Source/TableContent.m +++ b/Source/TableContent.m @@ -50,6 +50,7 @@ sortField = nil; areShowingAllRows = false; currentlyEditingRow = -1; + usedQuery = [[NSString stringWithString:@""] retain]; return self; } @@ -297,6 +298,8 @@ [limitRowsField intValue]-1, [prefs integerForKey:@"LimitResultsValue"]]]; } + [self setUsedQuery:query]; + queryResult = [mySQLConnection queryString:query]; if ( queryResult == nil ) { NSLog(@"Loading table data for %@ failed, query string was: %@", aTable, query); @@ -393,6 +396,9 @@ [limitRowsField intValue]-1, [prefs integerForKey:@"LimitResultsValue"]]]; [limitRowsField selectText:self]; } + + [self setUsedQuery:queryString]; + queryResult = [mySQLConnection queryString:queryString]; // [fullResult setArray:[[self fetchResultAsArray:queryResult] retain]]; [fullResult setArray:[self fetchResultAsArray:queryResult]]; @@ -605,6 +611,8 @@ [limitRowsField intValue]-1, [prefs integerForKey:@"LimitResultsValue"]]; } + [self setUsedQuery:queryString]; + theResult = [mySQLConnection queryString:queryString]; [filteredResult setArray:[self fetchResultAsArray:theResult]]; @@ -654,6 +662,18 @@ [argumentField setEnabled:(![[[compareField selectedItem] title] hasSuffix:@"NULL"])]; } +- (NSString *)usedQuery +{ + return usedQuery; +} + +- (void)setUsedQuery:(NSString *)query +{ + if(usedQuery) + [usedQuery release]; + usedQuery = [[NSString stringWithString:query] retain]; +} + #pragma mark Edit methods @@ -1659,6 +1679,7 @@ [limitRowsField intValue]-1, [prefs integerForKey:@"LimitResultsValue"]]]; } + [self setUsedQuery:queryString]; queryResult = [mySQLConnection queryString:queryString]; // [fullResult setArray:[[self fetchResultAsArray:queryResult] retain]]; [fullResult setArray:[self fetchResultAsArray:queryResult]]; @@ -2189,6 +2210,7 @@ objectValueForTableColumn:(NSTableColumn *)aTableColumn [compareType release]; if (sortField) [sortField release]; [prefs release]; + [usedQuery release]; [super dealloc]; } diff --git a/Source/TableDocument.h b/Source/TableDocument.h index f58faad5..ad66ef55 100644 --- a/Source/TableDocument.h +++ b/Source/TableDocument.h @@ -25,6 +25,7 @@ #import <Cocoa/Cocoa.h> #import <MCPKit_bundled/MCPKit_bundled.h> +#import <WebKit/WebKit.h> @class CMMCPConnection, CMMCPResult; @@ -96,6 +97,8 @@ NSToolbar *mainToolbar; NSToolbarItem *chooseDatabaseToolbarItem; + + WebView *printWebView; } //start sheet @@ -119,6 +122,8 @@ sshPort:(NSString *)sshPort; // no-longer in use - (NSMutableArray *)favorites; +- (NSString *)getHTMLforPrint; + //alert sheets method - (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo; diff --git a/Source/TableDocument.m b/Source/TableDocument.m index 19825570..49962fcc 100644 --- a/Source/TableDocument.m +++ b/Source/TableDocument.m @@ -42,6 +42,10 @@ #import "CMMCPConnection.h" #import "CMMCPResult.h" +//used for printing +#import "MGTemplateEngine.h" +#import "ICUTemplateMatcher.h" + NSString *TableDocumentFavoritesControllerSelectionIndexDidChange = @"TableDocumentFavoritesControllerSelectionIndexDidChange"; NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocumentFavoritesControllerFavoritesDidChange"; @@ -60,7 +64,8 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocum chooseDatabaseButton = nil; chooseDatabaseToolbarItem = nil; } - + printWebView = [[WebView alloc] init]; + [printWebView setFrameLoadDelegate:self]; return self; } @@ -97,14 +102,116 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocum [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } -- (NSPrintOperation *)printOperationWithSettings:(NSDictionary *)ps error:(NSError **)e -{ +- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame +{ + //because I need the webFrame loaded (for preview), I've moved the actuall printing here. NSPrintInfo *printInfo = [self printInfo]; [printInfo setHorizontalPagination:NSFitPagination]; [printInfo setVerticalPagination:NSAutoPagination]; - NSPrintOperation *printOp = [NSPrintOperation printOperationWithView:[[tableTabView selectedTabViewItem] view] printInfo:printInfo]; - return printOp; + [printInfo setVerticallyCentered:NO]; + [printInfo setTopMargin:30]; + [printInfo setBottomMargin:30]; + [printInfo setLeftMargin:10]; + [printInfo setRightMargin:10]; + + NSPrintOperation *op = [NSPrintOperation + printOperationWithView:[[[printWebView mainFrame] frameView] documentView] + printInfo:printInfo]; + + //add ability to select orientation to print panel + NSPrintPanel *printPanel = [op printPanel]; + [printPanel setOptions:[printPanel options] + NSPrintPanelShowsOrientation + NSPrintPanelShowsScaling]; + [op setPrintPanel:printPanel]; + + [op runOperationModalForWindow:tableWindow + delegate:self + didRunSelector: + @selector(printOperationDidRun:success:contextInfo:) + contextInfo:NULL]; + +} + +- (IBAction)printDocument:(id)sender +{ + //here load the printing document. The actual printing is done in the doneLoading delegate. + [[printWebView mainFrame] loadHTMLString:[self getHTMLforPrint] baseURL:nil]; +} + +- (void)printOperationDidRun:(NSPrintOperation *)printOperation + success:(BOOL)success + contextInfo:(void *)info +{ + //selector for print... maybe we can get rid of this? +} + +- (NSString *)getHTMLforPrint +{ + // Set up template engine with your chosen matcher. + MGTemplateEngine *engine = [MGTemplateEngine templateEngine]; + [engine setMatcher:[ICUTemplateMatcher matcherWithTemplateEngine:engine]]; + + NSString *versionForPrint = [NSString stringWithFormat:@"%@ %@ (build %@)", + [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"], + [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"], + [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"] + ]; + + NSMutableDictionary *connection = [[NSMutableDictionary alloc] init]; + if([[self user] length]) + [connection setValue:[self user] forKey:@"username"]; + [connection setValue:[self host] forKey:@"hostname"]; + if([[portField stringValue] length]) + [connection setValue:[portField stringValue] forKey:@"port"]; + [connection setValue:selectedDatabase forKey:@"database"]; + [connection setValue:versionForPrint forKey:@"version"]; + + NSArray *columns, *rows; + columns = rows = [[NSArray alloc] init]; + + if ( [tableTabView indexOfTabViewItem:[tableTabView selectedTabViewItem]] == 0 ){ + if([[tableSourceInstance tableStructureForPrint] count] > 0) + columns = [[NSArray alloc] initWithArray:[[tableSourceInstance tableStructureForPrint] objectAtIndex:0] copyItems:YES]; + if([[tableSourceInstance tableStructureForPrint] count] > 1) + rows = [[NSArray alloc] initWithArray: + [[tableSourceInstance tableStructureForPrint] objectsAtIndexes: + [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, [[tableSourceInstance tableStructureForPrint] count]-1)] + ] + ]; + } + else if ( [tableTabView indexOfTabViewItem:[tableTabView selectedTabViewItem]] == 1 ){ + if([[tableContentInstance currentResult] count] > 0) + columns = [[NSArray alloc] initWithArray:[[tableContentInstance currentResult] objectAtIndex:0] copyItems:YES]; + if([[tableContentInstance currentResult] count] > 1) + rows = [[NSArray alloc] initWithArray: + [[tableContentInstance currentResult] objectsAtIndexes: + [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, [[tableContentInstance currentResult] count]-1)] + ] + ]; + [connection setValue:[tableContentInstance usedQuery] forKey:@"query"]; + } + else if ( [tableTabView indexOfTabViewItem:[tableTabView selectedTabViewItem]] == 2 ){ + if([[customQueryInstance currentResult] count] > 0) + columns = [[NSArray alloc] initWithArray:[[customQueryInstance currentResult] objectAtIndex:0] copyItems:YES]; + if([[customQueryInstance currentResult] count] > 1) + rows = [[NSArray alloc] initWithArray: + [[customQueryInstance currentResult] objectsAtIndexes: + [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, [[customQueryInstance currentResult] count]-1)] + ] + ]; + [connection setValue:[customQueryInstance usedQuery] forKey:@"query"]; + } + + [engine setObject:connection forKey:@"c"]; + // Get path to template. + NSString *templatePath = [[NSBundle mainBundle] pathForResource:@"sequel-pro-print-template" ofType:@"html"]; + NSDictionary *print_data = [NSDictionary dictionaryWithObjectsAndKeys: + columns, @"columns", + rows, @"rows", + nil]; + // Process the template and display the results. + NSString *result = [engine processTemplateInFileAtPath:templatePath withVariables:print_data]; + return result; } - (CMMCPConnection *)sharedConnection @@ -1740,10 +1847,7 @@ NSString *TableDocumentFavoritesControllerFavoritesDidChange = @"TableDocum * Invoked when the document window is about to close */ - (void)windowWillClose:(NSNotification *)aNotification -{ - //reset print settings, so we're not prompted about saving them - [self setPrintInfo:[NSPrintInfo sharedPrintInfo]]; - +{ if ([mySQLConnection isConnected]) [self closeConnection]; if ([[[SPQueryConsole sharedQueryConsole] window] isVisible]) [self toggleConsole:self]; [[NSNotificationCenter defaultCenter] removeObserver:self]; diff --git a/Source/TableSource.h b/Source/TableSource.h index e97602f5..58964827 100644 --- a/Source/TableSource.h +++ b/Source/TableSource.h @@ -96,6 +96,7 @@ - (NSString *)defaultValueForField:(NSString *)field; - (NSArray *)fieldNames; - (NSDictionary *)enumFields; +- (NSArray *)tableStructureForPrint; //tableView datasource methods - (int)numberOfRowsInTableView:(NSTableView *)aTableView; diff --git a/Source/TableSource.m b/Source/TableSource.m index 92aa9811..de6bca6c 100644 --- a/Source/TableSource.m +++ b/Source/TableSource.m @@ -91,7 +91,7 @@ loads aTable, put it in an array, update the tableViewColumns and reload the tab [tableFields setArray:[self fetchResultAsArray:tableSourceResult]]; [tableSourceResult release]; - indexResult = [[mySQLConnection queryString:[NSString stringWithFormat:@"SHOW INDEX FROM %@", [selectedTable backtickQuotedString]]] retain]; + indexResult = [[mySQLConnection queryString:[NSString stringWithFormat:@"NSPrintPanelShowsPageSetupAccessory %@", [selectedTable backtickQuotedString]]] retain]; // [indexes setArray:[[self fetchResultAsArray:indexResult] retain]]; [indexes setArray:[self fetchResultAsArray:indexResult]]; [indexResult release]; @@ -832,6 +832,23 @@ returns a dictionary containing enum/set field names as key and possible values return [NSDictionary dictionaryWithDictionary:enumFields]; } +- (NSArray *)tableStructureForPrint +{ + CMMCPResult *queryResult; + NSMutableArray *tempResult = [NSMutableArray array]; + int i; + + queryResult = [mySQLConnection queryString:[NSString stringWithFormat:@"SHOW COLUMNS FROM %@", [selectedTable backtickQuotedString]]]; + + if ([queryResult numOfRows]) [queryResult dataSeek:0]; + [tempResult addObject:[queryResult fetchFieldNames]]; + for ( i = 0 ; i < [queryResult numOfRows] ; i++ ) { + [tempResult addObject:[queryResult fetchRowAsArray]]; + } + + return tempResult; +} + #pragma mark TableView datasource methods - (int)numberOfRowsInTableView:(NSTableView *)aTableView |