diff options
Diffstat (limited to 'Source/MGTemplateEngine.m')
-rw-r--r-- | Source/MGTemplateEngine.m | 673 |
1 files changed, 673 insertions, 0 deletions
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 |