Skip to content

Commit

Permalink
Implement parser for else chaining of helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
dmarcotte committed Apr 26, 2015
1 parent 45e5e51 commit 5bcb031
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 71 deletions.
1 change: 1 addition & 0 deletions handlebars/resources/messages/HbBundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ hb.parsing.element.expected.open=Expected Open "{{"
hb.parsing.element.expected.open_block=Expected Open Block "{{#"
hb.parsing.element.expected.open_end_block=Expected Open End Block "{{/"
hb.parsing.element.expected.open_inverse=Expected Open Inverse "{{^"
hb.parsing.element.expected.open_inverse_chain=Expected Open Inverse Chain "{{else"
hb.parsing.element.expected.open_partial=Expected Open Partial "{{>"
hb.parsing.element.expected.open_unescaped=Expected Open Unescaped "{{{"
hb.parsing.element.expected.open_sexpr=Expected Open Subexpression "("
Expand Down
139 changes: 69 additions & 70 deletions handlebars/src/com/dmarcotte/handlebars/parsing/HbParsing.java
Original file line number Diff line number Diff line change
Expand Up @@ -131,29 +131,23 @@ private boolean parseStatement(PsiBuilder builder) {

/**
* block
* : openBlock program inverseAndProgram? closeBlock
* : openBlock program inverseAndChain? closeBlock
* | openInverse program inverseAndProgram? closeBlock
*/
{
if (atOpenInverseExpression(builder)) {
PsiBuilder.Marker inverseBlockStartMarker = builder.mark();
PsiBuilder.Marker lookAheadMarker = builder.mark();
boolean isSimpleInverse = parseSimpleInverse(builder);
lookAheadMarker.rollbackTo();

if (isSimpleInverse) {
if (builder.getTokenType() == OPEN_INVERSE) {
if (builder.lookAhead(1) == CLOSE) {
/* HB_CUSTOMIZATION */
// leave this to be caught be the simpleInverseParser
inverseBlockStartMarker.rollbackTo();
// this is actually a `{{^}}` simple inverse. Bail out. It gets parsed outside of `statement`
return false;
}
else {
inverseBlockStartMarker.drop();
}

PsiBuilder.Marker blockMarker = builder.mark();
if (parseOpenInverse(builder)) {
parseProgramInverseProgramClose(builder, blockMarker);
parseProgram(builder);
parseInverseAndProgram(builder);
parseCloseBlock(builder);
blockMarker.done(HbTokenTypes.BLOCK_WRAPPER);
}
else {
return false;
Expand All @@ -165,7 +159,10 @@ private boolean parseStatement(PsiBuilder builder) {
if (tokenType == OPEN_BLOCK) {
PsiBuilder.Marker blockMarker = builder.mark();
if (parseOpenBlock(builder)) {
parseProgramInverseProgramClose(builder, blockMarker);
parseProgram(builder);
parseInverseChain(builder);
parseCloseBlock(builder);
blockMarker.done(HbTokenTypes.BLOCK_WRAPPER);
}
else {
return false;
Expand All @@ -183,6 +180,12 @@ private boolean parseStatement(PsiBuilder builder) {
*/
{
if (tokenType == OPEN) {
if (builder.lookAhead(1) == ELSE) {
/* HB_CUSTOMIZATION */
// this is actually an `{{else` expression, not a mustache.
return false;
}

parseMustache(builder, OPEN, CLOSE);
return true;
}
Expand Down Expand Up @@ -245,21 +248,30 @@ private boolean parseStatement(PsiBuilder builder) {
}

/**
* Helper method to take care of the business needed after an "open-type mustache" (openBlock or openInverse)
*
* Effective acts as the `program inverseAndProgram? closeBlock` part of the grammar
*
* <p/>
* NOTE: will resolve the given blockMarker
* inverseChain
* : openInverseChain program inverseChain?
* | inverseAndProgram
*/
private void parseProgramInverseProgramClose(PsiBuilder builder, PsiBuilder.Marker blockMarker) {
parseProgram(builder);
if (parseSimpleInverse(builder)) {
// if we have a simple inverse, must have more statements
parseStatements(builder);
private void parseInverseChain(PsiBuilder builder) {
if (!parseInverseAndProgram(builder)) {
if (parseOpenInverseChain(builder)) {
parseProgram(builder);
parseInverseChain(builder);
}
}
}

/**
* inverseAndProgram
* : INVERSE program
*/
private boolean parseInverseAndProgram(PsiBuilder builder) {
if (parseINVERSE(builder)) {
parseProgram(builder);
return true;
} else {
return false;
}
parseCloseBlock(builder);
blockMarker.done(HbTokenTypes.BLOCK_WRAPPER);
}

/**
Expand Down Expand Up @@ -301,6 +313,28 @@ private boolean parseOpenBlock(PsiBuilder builder) {
return true;
}

/**
* openInverseChain
* : OPEN_INVERSE_CHAIN sexpr CLOSE
* ;
*/
private boolean parseOpenInverseChain(PsiBuilder builder) {
PsiBuilder.Marker openInverseChainMarker = builder.mark();
if (!parseLeafToken(builder, OPEN)
|| !parseLeafToken(builder, ELSE)) {
openInverseChainMarker.rollbackTo();
return false;
}

if (parseSexpr(builder)) {
parseLeafTokenGreedy(builder, CLOSE);
}

openInverseChainMarker.done(OPEN_INVERSE_CHAIN);

return true;
}

/**
* openInverse
* : OPEN_INVERSE sexpr CLOSE
Expand All @@ -309,19 +343,8 @@ private boolean parseOpenBlock(PsiBuilder builder) {
private boolean parseOpenInverse(PsiBuilder builder) {
PsiBuilder.Marker openInverseBlockStacheMarker = builder.mark();

PsiBuilder.Marker regularInverseMarker = builder.mark();
if (!parseLeafToken(builder, OPEN_INVERSE)) {
// didn't find a standard open inverse token,
// check for the "{{else" version
regularInverseMarker.rollbackTo();
if (!parseLeafToken(builder, OPEN)
|| !parseLeafToken(builder, ELSE)) {
openInverseBlockStacheMarker.drop();
return false;
}
}
else {
regularInverseMarker.drop();
}

if (parseSexpr(builder)) {
Expand Down Expand Up @@ -436,15 +459,16 @@ protected void parsePartial(PsiBuilder builder) {
}

/**
* simpleInverse
* : OPEN_INVERSE CLOSE
* ;
* HB_CUSTOMIZATION: we don't parse an INVERSE token like the wycats/handlebars grammar since we lex "else" as
* an individual token so we can highlight it distinctly. This method parses {{^}} and {{else}}
* as a unit to synthesize INVERSE
*
*/
private boolean parseSimpleInverse(PsiBuilder builder) {
private boolean parseINVERSE(PsiBuilder builder) {
PsiBuilder.Marker simpleInverseMarker = builder.mark();
boolean isSimpleInverse;

// try and parse "{{^"
// try and parse "{{^}}"
PsiBuilder.Marker regularInverseMarker = builder.mark();
if (!parseLeafToken(builder, OPEN_INVERSE)
|| !parseLeafToken(builder, CLOSE)) {
Expand All @@ -456,7 +480,7 @@ private boolean parseSimpleInverse(PsiBuilder builder) {
isSimpleInverse = true;
}

// if we didn't find "{{^", check for "{{else"
// if we didn't find "{{^}}", check for "{{else}}"
PsiBuilder.Marker elseInverseMarker = builder.mark();
if (!isSimpleInverse
&& (!parseLeafToken(builder, OPEN)
Expand Down Expand Up @@ -932,29 +956,4 @@ private void recordLeafTokenError(IElementType expectedToken, PsiBuilder.Marker
unexpectedTokensMarker.error(HbBundle.message("hb.parsing.element.expected.invalid"));
}
}

/**
* Helper method to check whether the builder is an open inverse expression.
* <p/>
* An open inverse expression is either an OPEN_INVERSE token (i.e. "{{^"), or
* and OPEN token followed immediate by an ELSE token (i.e. "{{else")
*/
private boolean atOpenInverseExpression(PsiBuilder builder) {
boolean atOpenInverse = false;

if (builder.getTokenType() == OPEN_INVERSE) {
atOpenInverse = true;
}

PsiBuilder.Marker lookAheadMarker = builder.mark();
if (builder.getTokenType() == OPEN) {
builder.advanceLexer();
if (builder.getTokenType() == ELSE) {
atOpenInverse = true;
}
}

lookAheadMarker.rollbackTo();
return atOpenInverse;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ private HbTokenTypes() {
public static final IElementType OPEN_PARTIAL = new HbElementType("OPEN_PARTIAL", "hb.parsing.element.expected.open_partial");
public static final IElementType OPEN_ENDBLOCK = new HbElementType("OPEN_ENDBLOCK", "hb.parsing.element.expected.open_end_block");
public static final IElementType OPEN_INVERSE = new HbElementType("OPEN_INVERSE", "hb.parsing.element.expected.open_inverse");
public static final IElementType OPEN_INVERSE_CHAIN = new HbElementType("OPEN_INVERSE_CHAIN", "hb.parsing.element.expected.open_inverse_chain");
public static final IElementType OPEN_UNESCAPED = new HbElementType("OPEN_UNESCAPED", "hb.parsing.element.expected.open_unescaped");
public static final IElementType OPEN_SEXPR = new HbElementType("OPEN_SEXPR", "hb.parsing.element.expected.open_sexpr");
public static final IElementType CLOSE_SEXPR = new HbElementType("CLOSE_SEXPR", "hb.parsing.element.expected.close_sexpr");
Expand Down
1 change: 1 addition & 0 deletions handlebars/test/data/parser/MultipleInverseSections.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}
51 changes: 51 additions & 0 deletions handlebars/test/data/parser/MultipleInverseSections.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
HbFile:MultipleInverseSections.hbs
HbStatementsImpl(STATEMENTS)
HbBlockWrapperImpl(BLOCK_WRAPPER)
HbOpenBlockMustacheImpl(OPEN_BLOCK_STACHE)
HbPsiElementImpl([Hb] OPEN_BLOCK)
PsiElement([Hb] OPEN_BLOCK)('{{#')
HbMustacheNameImpl(MUSTACHE_NAME)
HbPathImpl(PATH)
HbPsiElementImpl([Hb] ID)
PsiElement([Hb] ID)('foo')
HbPsiElementImpl([Hb] CLOSE)
PsiElement([Hb] CLOSE)('}}')
HbStatementsImpl(STATEMENTS)
PsiElement([Hb] CONTENT)(' bar ')
HbPsiElementImpl([Hb] OPEN_INVERSE_CHAIN)
HbPsiElementImpl([Hb] OPEN)
PsiElement([Hb] OPEN)('{{')
HbPsiElementImpl([Hb] ELSE)
PsiElement([Hb] ELSE)('else')
PsiWhiteSpace(' ')
HbMustacheNameImpl(MUSTACHE_NAME)
HbPathImpl(PATH)
HbPsiElementImpl([Hb] ID)
PsiElement([Hb] ID)('if')
PsiWhiteSpace(' ')
HbParamImpl(PARAM)
HbPathImpl(PATH)
HbPsiElementImpl([Hb] ID)
PsiElement([Hb] ID)('bar')
HbPsiElementImpl([Hb] CLOSE)
PsiElement([Hb] CLOSE)('}}')
HbStatementsImpl(STATEMENTS)
<empty list>
HbSimpleInverseImpl(SIMPLE_INVERSE)
HbPsiElementImpl([Hb] OPEN)
PsiElement([Hb] OPEN)('{{')
HbPsiElementImpl([Hb] ELSE)
PsiElement([Hb] ELSE)('else')
HbPsiElementImpl([Hb] CLOSE)
PsiElement([Hb] CLOSE)('}}')
HbStatementsImpl(STATEMENTS)
PsiElement([Hb] CONTENT)(' baz ')
HbCloseBlockMustacheImpl(CLOSE_BLOCK_STACHE)
HbPsiElementImpl([Hb] OPEN_ENDBLOCK)
PsiElement([Hb] OPEN_ENDBLOCK)('{{/')
HbMustacheNameImpl(MUSTACHE_NAME)
HbPathImpl(PATH)
HbPsiElementImpl([Hb] ID)
PsiElement([Hb] ID)('foo')
HbPsiElementImpl([Hb] CLOSE)
PsiElement([Hb] CLOSE)('}}')
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{else foo}}bar{{/foo}}
29 changes: 29 additions & 0 deletions handlebars/test/data/parser/OldStandaloneInverseSection.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
HbFile:OldStandaloneInverseSection.hbs
HbStatementsImpl(STATEMENTS)
<empty list>
PsiErrorElement:Invalid
PsiElement([Hb] OPEN)('{{')
HbStatementsImpl(STATEMENTS)
<empty list>
PsiErrorElement:Invalid
PsiElement([Hb] ELSE)('else')
PsiWhiteSpace(' ')
HbStatementsImpl(STATEMENTS)
<empty list>
PsiErrorElement:Invalid
PsiElement([Hb] ID)('foo')
HbStatementsImpl(STATEMENTS)
<empty list>
PsiErrorElement:Invalid
PsiElement([Hb] CLOSE)('}}')
HbStatementsImpl(STATEMENTS)
PsiElement([Hb] CONTENT)('bar')
HbCloseBlockMustacheImpl(CLOSE_BLOCK_STACHE)
HbPsiElementImpl([Hb] OPEN_ENDBLOCK)
PsiElement([Hb] OPEN_ENDBLOCK)('{{/')
HbMustacheNameImpl(MUSTACHE_NAME)
HbPathImpl(PATH)
HbPsiElementImpl([Hb] ID)
PsiElement([Hb] ID)('foo')
HbPsiElementImpl([Hb] CLOSE)
PsiElement([Hb] CLOSE)('}}')
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

/**
* Java representations of the validations in Handlebars spec/parser.js
* (Precise revision: https://github.com/wycats/handlebars.js/blob/cb22ee5681b1eb1f89ee675651c018b77dd1524d/spec/parser.js)
* (Precise revision: https://github.com/wycats/handlebars.js/blob/4282668d47b90da0d00cf4c4a86977f18fc8cde4/spec/parser.js)
* <p/>
* The tests here should map pretty clearly by name to the `it "does something"` validations in parser.js.
* <p/>
Expand Down Expand Up @@ -98,6 +98,10 @@ public void testInverseElseStyleSection() {
doTest(true);
}

public void testMultipleInverseSections() {
doTest(true);
}

public void testEmptyBlocks() {
doTest(true);
}
Expand Down Expand Up @@ -130,6 +134,10 @@ public void testStandaloneInverseSection() {
doTest(true);
}

public void testOldStandaloneInverseSection() {
doTest(true);
}

/**
* Note on the spec/parser.js porting: some tests at the end are omitted
* because they make no sense in the context of the plugin
Expand Down

0 comments on commit 5bcb031

Please sign in to comment.