From 5bcb031b9742c25b9b8700830f6f3d449e28fcbd Mon Sep 17 00:00:00 2001 From: Daniel Marcotte Date: Fri, 24 Apr 2015 11:16:12 -0700 Subject: [PATCH] Implement parser for else chaining of helpers See https://github.com/wycats/handlebars.js/pull/892 for details --- .../resources/messages/HbBundle.properties | 1 + .../handlebars/parsing/HbParsing.java | 139 +++++++++--------- .../handlebars/parsing/HbTokenTypes.java | 1 + .../data/parser/MultipleInverseSections.hbs | 1 + .../data/parser/MultipleInverseSections.txt | 51 +++++++ .../parser/OldStandaloneInverseSection.hbs | 1 + .../parser/OldStandaloneInverseSection.txt | 29 ++++ .../handlebars/parsing/HbParserSpecTest.java | 10 +- 8 files changed, 162 insertions(+), 71 deletions(-) create mode 100644 handlebars/test/data/parser/MultipleInverseSections.hbs create mode 100644 handlebars/test/data/parser/MultipleInverseSections.txt create mode 100644 handlebars/test/data/parser/OldStandaloneInverseSection.hbs create mode 100644 handlebars/test/data/parser/OldStandaloneInverseSection.txt diff --git a/handlebars/resources/messages/HbBundle.properties b/handlebars/resources/messages/HbBundle.properties index 9e3d9bcc3a2..196f6df311c 100644 --- a/handlebars/resources/messages/HbBundle.properties +++ b/handlebars/resources/messages/HbBundle.properties @@ -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 "(" diff --git a/handlebars/src/com/dmarcotte/handlebars/parsing/HbParsing.java b/handlebars/src/com/dmarcotte/handlebars/parsing/HbParsing.java index c1c6597f744..85947601949 100644 --- a/handlebars/src/com/dmarcotte/handlebars/parsing/HbParsing.java +++ b/handlebars/src/com/dmarcotte/handlebars/parsing/HbParsing.java @@ -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; @@ -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; @@ -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; } @@ -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 - * - *

- * 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); } /** @@ -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 @@ -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)) { @@ -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)) { @@ -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) @@ -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. - *

- * 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; - } } diff --git a/handlebars/src/com/dmarcotte/handlebars/parsing/HbTokenTypes.java b/handlebars/src/com/dmarcotte/handlebars/parsing/HbTokenTypes.java index f04fb06ee88..a863ae07ea7 100644 --- a/handlebars/src/com/dmarcotte/handlebars/parsing/HbTokenTypes.java +++ b/handlebars/src/com/dmarcotte/handlebars/parsing/HbTokenTypes.java @@ -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"); diff --git a/handlebars/test/data/parser/MultipleInverseSections.hbs b/handlebars/test/data/parser/MultipleInverseSections.hbs new file mode 100644 index 00000000000..618e5361396 --- /dev/null +++ b/handlebars/test/data/parser/MultipleInverseSections.hbs @@ -0,0 +1 @@ +{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}} \ No newline at end of file diff --git a/handlebars/test/data/parser/MultipleInverseSections.txt b/handlebars/test/data/parser/MultipleInverseSections.txt new file mode 100644 index 00000000000..3fae969eeab --- /dev/null +++ b/handlebars/test/data/parser/MultipleInverseSections.txt @@ -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) + + 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)('}}') \ No newline at end of file diff --git a/handlebars/test/data/parser/OldStandaloneInverseSection.hbs b/handlebars/test/data/parser/OldStandaloneInverseSection.hbs new file mode 100644 index 00000000000..5fa66d93345 --- /dev/null +++ b/handlebars/test/data/parser/OldStandaloneInverseSection.hbs @@ -0,0 +1 @@ +{{else foo}}bar{{/foo}} \ No newline at end of file diff --git a/handlebars/test/data/parser/OldStandaloneInverseSection.txt b/handlebars/test/data/parser/OldStandaloneInverseSection.txt new file mode 100644 index 00000000000..472523ce7bb --- /dev/null +++ b/handlebars/test/data/parser/OldStandaloneInverseSection.txt @@ -0,0 +1,29 @@ +HbFile:OldStandaloneInverseSection.hbs + HbStatementsImpl(STATEMENTS) + + PsiErrorElement:Invalid + PsiElement([Hb] OPEN)('{{') + HbStatementsImpl(STATEMENTS) + + PsiErrorElement:Invalid + PsiElement([Hb] ELSE)('else') + PsiWhiteSpace(' ') + HbStatementsImpl(STATEMENTS) + + PsiErrorElement:Invalid + PsiElement([Hb] ID)('foo') + HbStatementsImpl(STATEMENTS) + + 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)('}}') \ No newline at end of file diff --git a/handlebars/test/src/com/dmarcotte/handlebars/parsing/HbParserSpecTest.java b/handlebars/test/src/com/dmarcotte/handlebars/parsing/HbParserSpecTest.java index 0230783c0d0..e127e9d5768 100644 --- a/handlebars/test/src/com/dmarcotte/handlebars/parsing/HbParserSpecTest.java +++ b/handlebars/test/src/com/dmarcotte/handlebars/parsing/HbParserSpecTest.java @@ -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) *

* The tests here should map pretty clearly by name to the `it "does something"` validations in parser.js. *

@@ -98,6 +98,10 @@ public void testInverseElseStyleSection() { doTest(true); } + public void testMultipleInverseSections() { + doTest(true); + } + public void testEmptyBlocks() { doTest(true); } @@ -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