Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
Fetching contributors…

Cannot retrieve contributors at this time

676 lines (575 sloc) 21.932 kb
//
// 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;
[matcher release];
matcher = 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
Jump to Line
Something went wrong with that request. Please try again.