diff --git a/src/handlebars-objc/ast/HBAstBlock.h b/src/handlebars-objc/ast/HBAstBlock.h index 063fa79..3056b5b 100644 --- a/src/handlebars-objc/ast/HBAstBlock.h +++ b/src/handlebars-objc/ast/HBAstBlock.h @@ -37,11 +37,11 @@ @property (retain, nonatomic) HBAstTag* elseTag; @property (retain, nonatomic) HBAstTag* closeTag; +@property (retain, nonatomic) NSMutableArray* elseBlocks; + @property (readonly, nonatomic) HBAstExpression* expression; // computed @property (retain, nonatomic) NSMutableArray* statements; @property (retain, nonatomic) NSMutableArray* inverseStatements; -@property BOOL invertedBlock; - @end diff --git a/src/handlebars-objc/ast/HBAstBlock.m b/src/handlebars-objc/ast/HBAstBlock.m index 1b2aca3..5790710 100644 --- a/src/handlebars-objc/ast/HBAstBlock.m +++ b/src/handlebars-objc/ast/HBAstBlock.m @@ -46,6 +46,7 @@ - (void) dealloc self.openTag = nil; self.elseTag = nil; self.closeTag = nil; + self.elseBlocks = nil; self.statements = nil; self.inverseStatements = nil; diff --git a/src/handlebars-objc/astVisitors/HBAstEvaluationVisitor.m b/src/handlebars-objc/astVisitors/HBAstEvaluationVisitor.m index 4880e29..b1e8694 100644 --- a/src/handlebars-objc/astVisitors/HBAstEvaluationVisitor.m +++ b/src/handlebars-objc/astVisitors/HBAstEvaluationVisitor.m @@ -211,6 +211,36 @@ - (void) evaluateContextualParametersInExpression:(HBAstExpression*)expression p } } +-(NSString*)evaluateHelperCallExpression:(HBAstBlock*)node forwardStatementsEvaluator:(HBStatementsEvaluator)forwardStatementsEvaluator inverseStatementsEvaluator:(HBStatementsEvaluator)inverseStatementsEvaluator +{ + HBHelper* helper = (HBHelper*)[self helperForExpression:node.expression]; + if (!helper) { + if (!self.error) // we report only one error for now. + self.error = [HBHelperMissingError HBHelperMissingErrorWithHelperName:[node.expression.mainValue.keyPath[0] key]]; + return nil; + } + + NSArray* positionalParameters = nil; + NSDictionary* namedParameters = nil; + [self evaluateContextualParametersInExpression:node.expression positionalParameters:&positionalParameters namedParameters:&namedParameters]; + + HBHelperCallingInfo* callingInfo = [[HBHelperCallingInfo alloc] init]; + + callingInfo.context = self.contextStack.current.context; + callingInfo.data = self.contextStack.current.dataContext; + callingInfo.positionalParameters = positionalParameters; + callingInfo.namedParameters = namedParameters; + callingInfo.statements = forwardStatementsEvaluator; + callingInfo.inverseStatements = inverseStatementsEvaluator; + callingInfo.template = self.template; + callingInfo.evaluationVisitor = self; + callingInfo.invocationKind = HBHelperInvocationBlock; + + NSString* helperResult = helper.block(callingInfo); + [callingInfo release]; + + return helperResult; +} #pragma mark - #pragma mark Visiting High-level nodes @@ -237,6 +267,42 @@ - (id) visitBlock:(HBAstBlock*)node }; HBStatementsEvaluator inverseStatementsEvaluator = ^(id context, HBDataContext* data) { + for( HBAstBlock *elseBlock in node.elseBlocks ) { + + if( elseBlock.expression == nil ) + { + // else only case + return evaluateStatements(nil, nil, elseBlock.inverseStatements, false); + } + else if( [self expressionIsHelperCall:elseBlock.expression]) { + + // This is a block helper. Evaluate expression params and invoke helper + id result = [self evaluateHelperCallExpression:elseBlock forwardStatementsEvaluator:^NSString*(id context, HBDataContext* data){ return @"!"; } inverseStatementsEvaluator:^NSString*(id context, HBDataContext* data){ return @""; }]; + if( [HBHelperUtils evaluateObjectAsBool:result] ) + return evaluateStatements(nil, nil, elseBlock.statements, false); + } + else + { + id evaluatedExpression = [self visitExpression:elseBlock.expression]; + + if ([HBHelperUtils isEnumerableByIndex:evaluatedExpression] ) { + // Array-like context + id arrayLike = evaluatedExpression; + for( id something in arrayLike ) + { + (void)something; // make compiler happy + return evaluateStatements(nil, nil, elseBlock.statements, false); + } + } else { + // String of scalar context + if ([HBHelperUtils evaluateObjectAsBool:evaluatedExpression]) { + return evaluateStatements(nil, nil, elseBlock.statements, false); + } + } + } + // try next else block + } + // no else block, try inverseStatements return evaluateStatements(nil, nil, node.inverseStatements, false); }; @@ -244,33 +310,7 @@ - (id) visitBlock:(HBAstBlock*)node if ([self expressionIsHelperCall:node.expression]) { // This is a block helper. Evaluate expression params and invoke helper - HBHelper* helper = (HBHelper*)[self helperForExpression:node.expression]; - if (!helper) { - if (!self.error) // we report only one error for now. - self.error = [HBHelperMissingError HBHelperMissingErrorWithHelperName:[node.expression.mainValue.keyPath[0] key]]; - return nil; - } - - NSArray* positionalParameters = nil; - NSDictionary* namedParameters = nil; - [self evaluateContextualParametersInExpression:node.expression positionalParameters:&positionalParameters namedParameters:&namedParameters]; - - HBHelperCallingInfo* callingInfo = [[HBHelperCallingInfo alloc] init]; - - callingInfo.context = self.contextStack.current.context; - callingInfo.data = self.contextStack.current.dataContext; - callingInfo.positionalParameters = positionalParameters; - callingInfo.namedParameters = namedParameters; - callingInfo.statements = forwardStatementsEvaluator; - callingInfo.inverseStatements = inverseStatementsEvaluator; - callingInfo.template = self.template; - callingInfo.evaluationVisitor = self; - callingInfo.invocationKind = HBHelperInvocationBlock; - - NSString* helperResult = helper.block(callingInfo); - [callingInfo release]; - - return helperResult; + return [self evaluateHelperCallExpression:node forwardStatementsEvaluator:forwardStatementsEvaluator inverseStatementsEvaluator:inverseStatementsEvaluator]; } else { // This is a normal block. diff --git a/src/handlebars-objc/astVisitors/HBAstParserPostprocessingVisitor.m b/src/handlebars-objc/astVisitors/HBAstParserPostprocessingVisitor.m index 4521bff..90e2bb1 100644 --- a/src/handlebars-objc/astVisitors/HBAstParserPostprocessingVisitor.m +++ b/src/handlebars-objc/astVisitors/HBAstParserPostprocessingVisitor.m @@ -105,9 +105,12 @@ - (id) visitBlock:(HBAstBlock*)node [self visitNode:statement]; } } - + + for( HBAstBlock *elseBlock in node.elseBlocks ) + [self visitBlock:elseBlock]; + [self processTag:node.closeTag]; - + return nil; } diff --git a/src/handlebars-objc/astVisitors/HBAstParserTestVisitor.m b/src/handlebars-objc/astVisitors/HBAstParserTestVisitor.m index 918d831..20f5b36 100644 --- a/src/handlebars-objc/astVisitors/HBAstParserTestVisitor.m +++ b/src/handlebars-objc/astVisitors/HBAstParserTestVisitor.m @@ -66,7 +66,7 @@ - (id) visitBlock:(HBAstBlock*)node add_cr(); } } - + if (node.inverseStatements) { [_result appendString:@" {{^}}\n"]; for (HBAstNode* statement in node.inverseStatements) { @@ -75,6 +75,16 @@ - (id) visitBlock:(HBAstBlock*)node add_cr(); } } + + if( node.elseBlocks.count > 0 ) { + [_result appendString:@" {{else}}\n"]; + for( HBAstBlock *elseBlock in node.elseBlocks ) { + [_result appendString:@" "]; + [self visitNode:elseBlock]; + add_cr(); + } + } + return nil; } diff --git a/src/handlebars-objc/helpers/HBBuiltinHelpersRegistry.m b/src/handlebars-objc/helpers/HBBuiltinHelpersRegistry.m index 10ad6fa..dc8bb13 100644 --- a/src/handlebars-objc/helpers/HBBuiltinHelpersRegistry.m +++ b/src/handlebars-objc/helpers/HBBuiltinHelpersRegistry.m @@ -33,6 +33,8 @@ #import "HBTemplate_Private.h" #import "HBEscapedString.h" +#import + static HBBuiltinHelpersRegistry* _builtinHelpersRegistry = nil; @interface HBBuiltinHelpersRegistry() @@ -165,7 +167,7 @@ + (void) registerEachHelper NSMutableString* result = [NSMutableString string]; for (id key in dictionaryLike) { dictionaryData[@"key"] = key; - id statementEvaluation = callingInfo.statements(dictionaryLike[key], dictionaryData); + id statementEvaluation = callingInfo.statements(((id(*)(id, SEL,NSString*))objc_msgSend)(dictionaryLike,@selector(objectForKeyedSubscript:),key), dictionaryData); if (statementEvaluation) [result appendString:statementEvaluation]; } [dictionaryData release]; diff --git a/src/handlebars-objc/helpers/HBHelperUtils.m b/src/handlebars-objc/helpers/HBHelperUtils.m index b27e8c9..eac9f33 100644 --- a/src/handlebars-objc/helpers/HBHelperUtils.m +++ b/src/handlebars-objc/helpers/HBHelperUtils.m @@ -101,6 +101,8 @@ + (BOOL)evaluateObjectAsBool:(id)object if ([object isKindOfClass:[NSNumber class]]) return [object boolValue]; if ([object isKindOfClass:[NSString class]]) return [object length] > 0; if ([object isKindOfClass:[NSArray class]]) return [object count] > 0; + if ([object isKindOfClass:[NSDictionary class]]) return [object count] > 0; + if ([object isKindOfClass:[NSSet class]]) return [object count] > 0; return false; } @@ -110,6 +112,8 @@ + (NSInteger)evaluateObjectAsInteger:(id)object if ([object isKindOfClass:[NSNumber class]]) return [object integerValue]; if ([object isKindOfClass:[NSString class]]) return [object length]; if ([object isKindOfClass:[NSArray class]]) return [object count]; + if ([object isKindOfClass:[NSDictionary class]]) return [object count]; + if ([object isKindOfClass:[NSSet class]]) return [object count]; return 0; } diff --git a/src/handlebars-objc/parser/handlebars-objc.lm b/src/handlebars-objc/parser/handlebars-objc.lm index ef8bcf7..0ff62ec 100644 --- a/src/handlebars-objc/parser/handlebars-objc.lm +++ b/src/handlebars-objc/parser/handlebars-objc.lm @@ -66,6 +66,11 @@ WS [ \t]* \{\{!\-\- { yy_push_state(DASHED_COMMENT, yyscanner); found(DASHED_COMMENT_START); } \-\-\}\} { yy_pop_state(yyscanner); found(DASHED_COMMENT_END); } + /* skip empty inverse sections */ +\{\{~?^~?\}\}\{\{\/ { yy_push_state(EXPRESSION, yyscanner); yylval->ival = 0; found(OPEN_ENDBLOCK); } +\{\{~?^~?\}\}\{\{~\/ { yy_push_state(EXPRESSION, yyscanner); yylval->ival = 1; found(OPEN_ENDBLOCK); } +\{\{~?else~?\}\}\{\{\/ { yy_push_state(EXPRESSION, yyscanner); yylval->ival = 0; found(OPEN_ENDBLOCK); } +\{\{~?else~?\}\}\{\{~\/ { yy_push_state(EXPRESSION, yyscanner); yylval->ival = 1; found(OPEN_ENDBLOCK); } \{\{ { yy_push_state(EXPRESSION, yyscanner); yylval->ival = 0; found(OPEN); } \{\{~ { yy_push_state(EXPRESSION, yyscanner); yylval->ival = 1; found(OPEN); } @@ -78,8 +83,13 @@ WS [ \t]* \{\{~\/ { yy_push_state(EXPRESSION, yyscanner); yylval->ival = 1; found(OPEN_ENDBLOCK); } \{\{^ { yy_push_state(EXPRESSION, yyscanner); yylval->ival = 0; found(OPEN_INVERSE); } \{\{~^ { yy_push_state(EXPRESSION, yyscanner); yylval->ival = 1; found(OPEN_INVERSE); } -\{\{[ ]*else { yy_push_state(EXPRESSION, yyscanner); yylval->ival = 0; found(OPEN_INVERSE); } -\{\{~[ ]*else { yy_push_state(EXPRESSION, yyscanner); yylval->ival = 1; found(OPEN_INVERSE); } +\{\{[ ]*else[ ]*\}\} { yylval->ival = 0; found(INVERSE); } +\{\{~[ ]*else[ ]*\}\} { yylval->ival = 1; found(INVERSE); } +\{\{[ ]*else[ ]*~\}\} { yylval->ival = 2; found(INVERSE); } +\{\{~[ ]*else[ ]*~\}\} { yylval->ival = 3; found(INVERSE); } + +\{\{[ ]*else { yy_push_state(EXPRESSION, yyscanner); yylval->ival = 0; found(OPEN_INVERSE_CHAIN); } +\{\{~?[ ]*else { yy_push_state(EXPRESSION, yyscanner); yylval->ival = 1; found(OPEN_INVERSE_CHAIN); } \{\{\{ { yy_push_state(EXPRESSION, yyscanner); yylval->ival = 0; found(OPEN_UNESCAPED); } \{\{~\{ { yy_push_state(EXPRESSION, yyscanner); yylval->ival = 1; found(OPEN_UNESCAPED); } \{\{& { yy_push_state(EXPRESSION, yyscanner); yylval->ival = 0; found(OPEN_UNESCAPED_AMPERSAND); } @@ -112,7 +122,7 @@ WS [ \t]* "."/[\}\/ \t\)~] { yylval->nsString = nsString(yytext); found(ID); } ".." { yylval->nsString = nsString(yytext); found(ID); } [\/\.] { yylval->nsString = nsString(yytext); found(PATH_SEPARATOR); } - {ID}+/[=\/\. \t\}\)~] { yylval->nsString = nsString(yytext); found(ID); } + {ID}+/[=\/\. \t\}\)~\(] { yylval->nsString = nsString(yytext); found(ID); } \[[^\[]*\] { NSString* s = nsString(yytext); yylval->nsString = [s substringWithRange:NSMakeRange(1, s.length -2)]; found(ID); } /* XXX must remove [] */ {WS}* {} . { found(UNKNOWN); } diff --git a/src/handlebars-objc/parser/handlebars-objc.ym b/src/handlebars-objc/parser/handlebars-objc.ym index 7ec76a7..de8df98 100644 --- a/src/handlebars-objc/parser/handlebars-objc.ym +++ b/src/handlebars-objc/parser/handlebars-objc.ym @@ -52,12 +52,12 @@ %token PATH_SEPARATOR %token COMMENT_CONTENT -%type statements; +%type statements inverseChain; %type program statement; %type path_component; %type value; %type path contextual_value; -%type block_tag_open block_tag_close inverse_tag_open else_tag rawblock_tag_open rawblock_tag_close; +%type block_tag_open block_tag_close inverse_tag_open inverse_chain_tag_open rawblock_tag_open rawblock_tag_close; %type simple_tag; %type simple_expression expression_with_positional_values expression; %type raw_text; @@ -69,7 +69,7 @@ %type data; %type partial_name; %type partial_tag; -%type block_tag else_tag_block; +%type block_tag inverseAndProgram_tag; %type comment %type comment_content; %type hash; @@ -88,6 +88,8 @@ %token OPEN_BLOCK %token OPEN_ENDBLOCK %token OPEN_INVERSE +%token OPEN_INVERSE_CHAIN +%token INVERSE %token OPEN_UNESCAPED %token OPEN_UNESCAPED_AMPERSAND %token CLOSE @@ -182,12 +184,27 @@ block_tag_close : OPEN_ENDBLOCK expression CLOSE { HBAstTag* tag = [[HBAstTag new] autorelease]; tag.left_wsc = $1; tag.right_wsc = $3; tag.expression = $2; $$ = tag; } ; -else_tag -: OPEN_INVERSE CLOSE { HBAstTag* tag = [[HBAstTag new] autorelease]; tag.left_wsc = $1; tag.right_wsc = $2; $$ = tag;} +inverse_chain_tag_open +: OPEN_INVERSE_CHAIN expression CLOSE { HBAstTag* tag = [[HBAstTag new] autorelease]; tag.left_wsc = $1; tag.expression = $2; tag.right_wsc = $3; $$ = tag;} +; + +inverseAndProgram_tag +: INVERSE statements { + + HBAstTag* b = [[HBAstTag new] autorelease]; + b.left_wsc = $1 == 1 || $1 == 3; + b.right_wsc = $1 == 2 || $1 == 3; + + HBAstBlock *e = [[HBAstBlock new] autorelease]; + e.openTag = b; + e.inverseStatements = $2; + + $$ = e; } ; inverse_tag_open : OPEN_INVERSE expression CLOSE { HBAstTag* tag = [[HBAstTag new] autorelease]; tag.left_wsc = $1; tag.right_wsc = $3; tag.expression = $2; $$ = tag; } +| OPEN_INVERSE CLOSE { HBAstTag* tag = [[HBAstTag new] autorelease]; tag.left_wsc = $1; tag.right_wsc = $2; $$ = tag; } ; rawblock_tag_open @@ -212,17 +229,55 @@ block_tag b.closeTag = $2; $$ = b; } -| block_tag_open statements else_tag_block { - HBAstBlock* b = $3; +| block_tag_open statements inverseAndProgram_tag block_tag_close { + HBAstBlock* b = [[HBAstBlock new] autorelease]; b.openTag = $1; + b.closeTag = $4; b.statements = $2; + b.inverseStatements = $3.inverseStatements; + b.elseTag = $3.openTag; $$ = b; } -| block_tag_open else_tag_block { - HBAstBlock* b = $2; +| block_tag_open inverseAndProgram_tag block_tag_close { + HBAstBlock* b = [[HBAstBlock new] autorelease]; b.openTag = $1; + b.closeTag = $3; + b.inverseStatements = $2.inverseStatements; + b.elseTag = $2.openTag; $$ = b; } +| block_tag_open statements inverseChain inverseAndProgram_tag block_tag_close { + HBAstBlock* b = [[HBAstBlock new] autorelease]; + b.openTag = $1; + b.closeTag = $5; + b.statements = $2; + b.elseBlocks = $3; + [b.elseBlocks addObject:$4]; + $$ = b; +} +| block_tag_open inverseChain inverseAndProgram_tag block_tag_close { + HBAstBlock* b = [[HBAstBlock new] autorelease]; + b.openTag = $1; + b.closeTag = $4; + b.elseBlocks = $2; + [b.elseBlocks addObject:$3]; + $$ = b; +} +| block_tag_open statements inverseChain block_tag_close { + HBAstBlock* b = [[HBAstBlock new] autorelease]; + b.openTag = $1; + b.closeTag = $4; + b.statements = $2; + b.elseBlocks = $3; + $$ = b; +} +| block_tag_open inverseChain block_tag_close { + HBAstBlock* b = [[HBAstBlock new] autorelease]; + b.openTag = $1; + b.closeTag = $3; + b.elseBlocks = $2; + $$ = b; +} | inverse_tag_open statements block_tag_close { HBAstBlock* b = [[HBAstBlock new] autorelease]; b.openTag = $1; @@ -243,22 +298,68 @@ block_tag b.closeTag = $3; $$ = b; } -; - -else_tag_block -: else_tag statements block_tag_close { +| block_tag_open inverse_tag_open statements block_tag_close { HBAstBlock* b = [[HBAstBlock new] autorelease]; - b.elseTag = $1; - b.inverseStatements = $2; - b.closeTag = $3; + b.openTag = $1; + b.closeTag = $4; + + if( $2.expression == nil ) + { + b.inverseStatements = $3; + b.elseTag = $2; + } + else + { + HBAstBlock *e = [[HBAstBlock new] autorelease]; + e.openTag = $2; + e.inverseStatements = $3; + + b.elseBlocks = [NSMutableArray arrayWithObject:e]; + } + $$ = b; } -| else_tag block_tag_close { +| block_tag_open statements inverse_tag_open statements block_tag_close { + HBAstBlock* b = [[HBAstBlock new] autorelease]; - b.elseTag = $1; - b.closeTag = $2; + b.openTag = $1; + b.closeTag = $5; + b.statements = $2; + + if( $3.expression == nil ) + { + b.inverseStatements = $4; + b.elseTag = $3; + } + else + { + HBAstBlock *e = [[HBAstBlock new] autorelease]; + e.openTag = $3; + e.inverseStatements = $4; + + b.elseBlocks = [NSMutableArray arrayWithObject:e]; + } + $$ = b; -}; +} +; + + +inverseChain +: inverseChain inverse_chain_tag_open statements { + HBAstBlock *b = [[HBAstBlock new] autorelease]; + b.openTag = $2; + b.statements = $3; + [$1 addObject:b]; + $$ = $1; + } +| inverse_chain_tag_open statements { + HBAstBlock *b = [[HBAstBlock new] autorelease]; + b.openTag = $1; + b.statements = $2; + $$ = [NSMutableArray arrayWithObject:b]; + } +; simple_expression : contextual_value { HBAstExpression* expression = [[HBAstExpression new] autorelease]; expression.mainValue = $1; $$ = expression; } diff --git a/src/handlebars-objcTests/HBTestHelpers.m b/src/handlebars-objcTests/HBTestHelpers.m index 9903ad4..7f66b8a 100644 --- a/src/handlebars-objcTests/HBTestHelpers.m +++ b/src/handlebars-objcTests/HBTestHelpers.m @@ -586,6 +586,23 @@ - (void) testIssue5 +- (void) testElseIf +{ + id string = @"{{#if para1}}one{{else if para2}}two{{else if para3}}three{{else}}none{{/ifeq}}"; + + NSString* result = renderWithHelpers(string, @{ @"para1" : @1 }, nil); + XCTAssertEqualObjects(result, @"one"); + + result = renderWithHelpers(string, @{ @"para2" : @1 }, nil); + XCTAssertEqualObjects(result, @"two"); + + result = renderWithHelpers(string, @{ @"para3" : @1 }, nil); + XCTAssertEqualObjects(result, @"three"); + + result = renderWithHelpers(string, @{ }, nil); + XCTAssertEqualObjects(result, @"none"); +} + /// Unported tests from handlebars.js #if 0 diff --git a/src/handlebars-objcTests/HBTestParser.m b/src/handlebars-objcTests/HBTestParser.m index 8d7718f..2d9752a 100644 --- a/src/handlebars-objcTests/HBTestParser.m +++ b/src/handlebars-objcTests/HBTestParser.m @@ -343,4 +343,13 @@ - (void) testRawBlockParsing XCTAssertEqualObjects([self astString:@"aaa {{{{foo}}}} {{a}} {{{{/foo}}}} bbbb" error:&error], @"CONTENT[ 'aaa ' ]\nBLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' {{a}} ' ]\n\nCONTENT[ ' bbbb' ]\n"); XCTAssert(!error, @"evaluation should not generate an error"); } + +- (void) testMissingWhiteSpaceInExpression +{ + NSError* error = nil; + + NSString *result = [self astString:@" {{#if(hello)}}true{{/if}}" error:&error]; + XCTAssertEqualObjects(result, @"CONTENT[ ' ' ]\nBLOCK:\n {{ ID:if [ID:hello []] }}\n PROGRAM:\n CONTENT[ 'true' ]\n\n"); + XCTAssert(!error, @"evaluation should not generate an error"); +} @end