// // 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 "NSArray_DeepMutableCopy.h" #import "NSDictionary_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 () - (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:(NSInteger)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 *)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 *)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:(NSInteger)code continuing:(BOOL)continuing { if (delegate) { NSString *errStr; if(errorStr) errStr = [NSString stringWithString:errorStr]; else errStr = NSLocalizedString(@"MGTemplateEngine Error", @"mgtemplateengine error"); if (!continuing) errStr = [NSString stringWithFormat:@"%@: %@", NSLocalizedString(@"Fatal Error", @"fatal error"), errStr]; SEL selector = @selector(templateEngine:encounteredError:isContinuing:); if ([(NSObject *)delegate respondsToSelector:selector]) { NSError *error = [NSError errorWithDomain:TEMPLATE_ENGINE_ERROR_DOMAIN code:code userInfo:@{NSLocalizedDescriptionKey : errStr}]; [(NSObject *)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. NSInteger numKeys = [dotBits count]; if (numKeys > 1) { // otherwise no point in checking NSObject *thisParent = currObj; NSString *thisKey = nil; for (NSInteger 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) { NSInteger indexValue = [digits integerValue]; if (indexValue >= 0 && indexValue < (NSInteger)[((NSArray *)currObj) count]) { newObj = [((NSArray *)currObj) objectAtIndex:indexValue]; } } } 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 integerValue] 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:@YES forKey:@"true"]; [_globals setObject:@NO forKey:@"false"]; [_globals setObject:@YES forKey:@"YES"]; [_globals setObject:@NO forKey:@"NO"]; [_globals setObject:@YES forKey:@"yes"]; [_globals setObject:@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 *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 = @[]; } [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 *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. NSInteger openBlocks = [_openBlocksStack count]; if (openBlocks > 0) { NSString *errMsg = [NSString stringWithFormat:@"Finished processing template, but %ld %@ left open (%@).", (long)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