diff --git a/web-view-components-compiler/src/main/antlr/WebViewComponentsLexerBase.g4 b/web-view-components-compiler/src/main/antlr/WebViewComponentsLexerBase.g4 index 8dfacd5..5cf9db7 100644 --- a/web-view-components-compiler/src/main/antlr/WebViewComponentsLexerBase.g4 +++ b/web-view-components-compiler/src/main/antlr/WebViewComponentsLexerBase.g4 @@ -23,6 +23,7 @@ channels { @header { import java.util.Set; + import groowt.view.component.web.WebViewComponentBugError; import static groowt.view.component.web.antlr.LexerSemanticPredicates.*; } @@ -109,6 +110,10 @@ channels { this.pushMode(GROOVY_CODE); } + private boolean inAttrComponent() { + return this.peekMode(1) == COMPONENT_ATTR_VALUE; + } + } // ---------------------------------------- @@ -262,11 +267,30 @@ TagStartError mode IN_TAG; ComponentClose - : GT -> popMode + : GT + { + if (this.inAttrComponent() && this.attrComponentFinished()) { + this.popMode(); + this.popMode(); + } else { + this.popMode(); + } + } ; ComponentSelfClose - : FS GT -> popMode + : FS GT + { + if (this.inAttrComponent()) { + this.exitAttrComponent(); + if (this.attrComponentFinished()) { + this.popMode(); + this.popMode(); + } + } else { + this.popMode(); + } + } ; ConstructorOpen @@ -313,10 +337,14 @@ ClosureAttrValueStart ComponentAttrValueStart : LEFT_CURLY InTagNlws? { this.isNext('<') }? + { + this.enterAttrComponent(); + this.pushMode(COMPONENT_ATTR_VALUE); + } ; ComponentAttrValueEnd - : GT RIGHT_CURLY + : InTagNlws? RIGHT_CURLY { this.popAttrComponent(); } ; InTagNlws @@ -327,6 +355,85 @@ TagError : . -> channel(ERROR) ; +// ---------------------------------------- +mode COMPONENT_ATTR_VALUE; + +AttrComponentOpen + : LT { !isAnyOf(this.getNextChar(), '/', '>') }? -> type(ComponentOpen), pushMode(TAG_START) + ; + +AttrClosingComponentOpen + : LT FS { !this.isNext('>') }? + { + this.exitAttrComponent(); + } -> type(ClosingComponentOpen), pushMode(TAG_START) + ; + +AttrFragmentOpen + : LT GT -> type(FragmentOpen) + ; + +AttrFragmentClose + : LT FS GT { + this.exitAttrComponent(); + } -> type(FragmentClose), popMode + ; + +AttrEqualsScriptletOpen + : LT PERCENT EQ -> type(EqualsScriptletOpen), pushMode(GROOVY_CODE) + ; + +AttrPlainScriptletOpen + : LT PERCENT -> type(PlainScriptletOpen), pushMode(GROOVY_CODE) + ; + +AttrDollarScriptletOpen + : DOLLAR LEFT_CURLY { + this.curlies.push(() -> { + this.setType(DollarScriptletClose); + this.popMode(); + }); + this.curlies.increment(); + this.setType(DollarScriptletOpen); + this.pushMode(GROOVY_CODE); + } + ; + +AttrDollarReferenceStart + : DOLLAR { isGStringIdentifierStartChar(this.getNextChar()) }? + -> type(DollarReferenceStart), pushMode(IN_G_STRING_PATH) + ; + +AttrQuestionTagOpen + : LT QUESTION -> type(QuestionTagOpen) + ; + +AttrQuestionTagClose + : QUESTION GT -> type(QuestionTagClose) + ; + +AttrHtmlCommentOpen + : LT BANG TWO_DASH -> type(HtmlCommentOpen) + ; + +AttrHtmlCommentClose + : TWO_DASH GT -> type(HtmlCommentClose) + ; + +AttrRawText + : ( ~[-<$?] + | MINUS { !this.isNext("->") }? + | LT { canFollowLessThan(this.getNextCharAsString()) }? + | LT BANG { !this.isNext("--") }? + | DOLLAR { !(this.isNext('{') || isIdentifierStartChar(this.getNextChar())) }? + | QUESTION { !this.isNext('>') }? + )+ -> type(RawText) + ; + +AttrError + : . -> type(ErrorChar), channel(ERROR) + ; + // ---------------------------------------- mode GROOVY_CODE; @@ -542,8 +649,11 @@ GStringPathEnd yield new StringContinueEndSpec(); } } + case COMPONENT_ATTR_VALUE -> new StringContinueEndSpec(); case MAIN -> new StringContinueEndSpec(); - default -> throw new IllegalStateException("not a valid gString context: " + this.getModeName(this.peekMode(1))); + default -> throw new IllegalStateException( + "not a valid gStringPath context: " + this.getModeName(this.peekMode(1)) + ); }; switch (endSpec) { case StringContinueEndSpec ignored -> { diff --git a/web-view-components-compiler/src/main/java/groowt/view/component/web/antlr/AbstractWebViewComponentsLexer.java b/web-view-components-compiler/src/main/java/groowt/view/component/web/antlr/AbstractWebViewComponentsLexer.java index 489b624..62b795d 100644 --- a/web-view-components-compiler/src/main/java/groowt/view/component/web/antlr/AbstractWebViewComponentsLexer.java +++ b/web-view-components-compiler/src/main/java/groowt/view/component/web/antlr/AbstractWebViewComponentsLexer.java @@ -1,6 +1,7 @@ package groowt.view.component.web.antlr; import groovyjarjarantlr4.runtime.Token; +import groowt.view.component.web.WebViewComponentBugError; import org.antlr.v4.runtime.CharStream; import org.antlr.v4.runtime.Lexer; import org.antlr.v4.runtime.atn.ATN; @@ -13,6 +14,7 @@ import org.slf4j.LoggerFactory; import java.util.Arrays; import java.util.Deque; import java.util.LinkedList; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.stream.Collectors; @@ -57,6 +59,8 @@ public abstract class AbstractWebViewComponentsLexer extends Lexer { private boolean canPreamble = true; private boolean inPreamble; private boolean inConstructor; + private Deque attrComponentFinishedStack = new LinkedList<>(); + public AbstractWebViewComponentsLexer(CharStream input) { super(input); @@ -115,6 +119,14 @@ public abstract class AbstractWebViewComponentsLexer extends Lexer { this.inConstructor = inConstructor; } + public Deque getAttrComponentFinishedStack() { + return this.attrComponentFinishedStack; + } + + public void setAttrComponentFinishedStack(Deque attrComponentFinishedStack) { + this.attrComponentFinishedStack = attrComponentFinishedStack; + } + @Override public void reset() { this.curlies.clear(); @@ -122,6 +134,7 @@ public abstract class AbstractWebViewComponentsLexer extends Lexer { this.canPreamble = true; this.inPreamble = false; this.inConstructor = false; + this.attrComponentFinishedStack = new LinkedList<>(); super.reset(); } @@ -204,6 +217,30 @@ public abstract class AbstractWebViewComponentsLexer extends Lexer { this.inConstructor = false; } + protected void enterAttrComponent() { + this.attrComponentFinishedStack.push(new AtomicBoolean()); + } + + protected void exitAttrComponent() { + final AtomicBoolean attrComponentFinished = this.attrComponentFinishedStack.peek(); + if (attrComponentFinished == null) { + throw new WebViewComponentBugError(new IllegalStateException()); + } + attrComponentFinished.set(true); + } + + protected boolean attrComponentFinished() { + final AtomicBoolean attrComponentFinished = this.attrComponentFinishedStack.peek(); + if (attrComponentFinished == null) { + throw new WebViewComponentBugError(new IllegalStateException()); + } + return attrComponentFinished.get(); + } + + protected void popAttrComponent() { + this.attrComponentFinishedStack.pop(); + } + protected String getNextCharsAsString(int numberOfChars) { final var b = new StringBuilder(); for (int i = 1; i <= numberOfChars; i++) { @@ -224,6 +261,10 @@ public abstract class AbstractWebViewComponentsLexer extends Lexer { return this._input.LA(1); } + protected boolean isPrevious(char test) { + return this._input.LA(-2) == test; + } + protected boolean isNext(char test) { return this._input.LA(1) == test; } diff --git a/web-view-components-compiler/src/test/lexer/componentValue.wvc b/web-view-components-compiler/src/test/lexer/componentValue.wvc new file mode 100644 index 0000000..d2c4f48 --- /dev/null +++ b/web-view-components-compiler/src/test/lexer/componentValue.wvc @@ -0,0 +1 @@ +$it

} /> diff --git a/web-view-components-compiler/src/test/lexer/componentValueNested.wvc b/web-view-components-compiler/src/test/lexer/componentValueNested.wvc new file mode 100644 index 0000000..5dde995 --- /dev/null +++ b/web-view-components-compiler/src/test/lexer/componentValueNested.wvc @@ -0,0 +1 @@ +} />} /> diff --git a/web-view-components-compiler/src/test/lexer/componentValueNestedP.wvc b/web-view-components-compiler/src/test/lexer/componentValueNestedP.wvc new file mode 100644 index 0000000..d0f2cda --- /dev/null +++ b/web-view-components-compiler/src/test/lexer/componentValueNestedP.wvc @@ -0,0 +1 @@ +$it

} />} /> diff --git a/web-view-components-compiler/src/test/lexer/componentValueSelfClosing.wvc b/web-view-components-compiler/src/test/lexer/componentValueSelfClosing.wvc new file mode 100644 index 0000000..4c11a85 --- /dev/null +++ b/web-view-components-compiler/src/test/lexer/componentValueSelfClosing.wvc @@ -0,0 +1 @@ +} /> diff --git a/web-view-components-compiler/src/test/lexer/componentValueWithNlws.wvc b/web-view-components-compiler/src/test/lexer/componentValueWithNlws.wvc new file mode 100644 index 0000000..3ce0887 --- /dev/null +++ b/web-view-components-compiler/src/test/lexer/componentValueWithNlws.wvc @@ -0,0 +1 @@ + } /> diff --git a/web-view-components-compiler/src/test/lexer/tokens-files/componentValueNestedP_tokens.txt b/web-view-components-compiler/src/test/lexer/tokens-files/componentValueNestedP_tokens.txt new file mode 100644 index 0000000..201b635 --- /dev/null +++ b/web-view-components-compiler/src/test/lexer/tokens-files/componentValueNestedP_tokens.txt @@ -0,0 +1,27 @@ +0: ComponentOpen[1,1](<) +1: TypedIdentifier[1,2](Test) +2: Nlws[1,6]( ) +3: AttributeIdentifier[1,7](transform) +4: Equals[1,16](=) +5: ComponentAttrValueStart[1,17]({) +6: ComponentOpen[1,18](<) +7: TypedIdentifier[1,19](Test) +8: Nlws[1,23]( ) +9: AttributeIdentifier[1,24](transform) +10: Equals[1,33](=) +11: ComponentAttrValueStart[1,34]({) +12: ComponentOpen[1,35](<) +13: StringIdentifier[1,36](p) +14: ComponentClose[1,37](>) +15: DollarReferenceStart[1,38]($) +16: GroovyCode[1,39](it) +17: ClosingComponentOpen[1,41]() +20: ComponentAttrValueEnd[1,45](}) +21: Nlws[1,46]( ) +22: ComponentSelfClose[1,47](/>) +23: ComponentAttrValueEnd[1,49](}) +24: Nlws[1,50]( ) +25: ComponentSelfClose[1,51](/>) +26: RawText[1,53](\n) \ No newline at end of file diff --git a/web-view-components-compiler/src/test/lexer/tokens-files/componentValueNested_tokens.txt b/web-view-components-compiler/src/test/lexer/tokens-files/componentValueNested_tokens.txt new file mode 100644 index 0000000..475ea8b --- /dev/null +++ b/web-view-components-compiler/src/test/lexer/tokens-files/componentValueNested_tokens.txt @@ -0,0 +1,23 @@ +0: ComponentOpen[1,1](<) +1: TypedIdentifier[1,2](Test) +2: Nlws[1,6]( ) +3: AttributeIdentifier[1,7](transform) +4: Equals[1,16](=) +5: ComponentAttrValueStart[1,17]({) +6: ComponentOpen[1,18](<) +7: TypedIdentifier[1,19](Test) +8: Nlws[1,23]( ) +9: AttributeIdentifier[1,24](transform) +10: Equals[1,33](=) +11: ComponentAttrValueStart[1,34]({) +12: ComponentOpen[1,35](<) +13: TypedIdentifier[1,36](Test) +14: Nlws[1,40]( ) +15: ComponentSelfClose[1,41](/>) +16: ComponentAttrValueEnd[1,43](}) +17: Nlws[1,44]( ) +18: ComponentSelfClose[1,45](/>) +19: ComponentAttrValueEnd[1,47](}) +20: Nlws[1,48]( ) +21: ComponentSelfClose[1,49](/>) +22: RawText[1,51](\n) \ No newline at end of file diff --git a/web-view-components-compiler/src/test/lexer/tokens-files/componentValueSelfClosing_tokens.txt b/web-view-components-compiler/src/test/lexer/tokens-files/componentValueSelfClosing_tokens.txt new file mode 100644 index 0000000..ea7e087 --- /dev/null +++ b/web-view-components-compiler/src/test/lexer/tokens-files/componentValueSelfClosing_tokens.txt @@ -0,0 +1,14 @@ +0: ComponentOpen[1,1](<) +1: TypedIdentifier[1,2](Test) +2: Nlws[1,6]( ) +3: AttributeIdentifier[1,7](transform) +4: Equals[1,16](=) +5: ComponentAttrValueStart[1,17]({) +6: ComponentOpen[1,18](<) +7: TypedIdentifier[1,19](Test) +8: Nlws[1,23]( ) +9: ComponentSelfClose[1,24](/>) +10: ComponentAttrValueEnd[1,26](}) +11: Nlws[1,27]( ) +12: ComponentSelfClose[1,28](/>) +13: RawText[1,30](\n) \ No newline at end of file diff --git a/web-view-components-compiler/src/test/lexer/tokens-files/componentValueWithNlws_tokens.txt b/web-view-components-compiler/src/test/lexer/tokens-files/componentValueWithNlws_tokens.txt new file mode 100644 index 0000000..98e7fba --- /dev/null +++ b/web-view-components-compiler/src/test/lexer/tokens-files/componentValueWithNlws_tokens.txt @@ -0,0 +1,14 @@ +0: ComponentOpen[1,1](<) +1: TypedIdentifier[1,2](Test) +2: Nlws[1,6]( ) +3: AttributeIdentifier[1,7](test) +4: Equals[1,11](=) +5: ComponentAttrValueStart[1,12]({ ) +6: ComponentOpen[1,14](<) +7: TypedIdentifier[1,15](Test) +8: Nlws[1,19]( ) +9: ComponentSelfClose[1,20](/>) +10: ComponentAttrValueEnd[1,22]( }) +11: Nlws[1,24]( ) +12: ComponentSelfClose[1,25](/>) +13: RawText[1,27](\n) \ No newline at end of file diff --git a/web-view-components-compiler/src/test/lexer/tokens-files/componentValue_tokens.txt b/web-view-components-compiler/src/test/lexer/tokens-files/componentValue_tokens.txt new file mode 100644 index 0000000..f1c1f5e --- /dev/null +++ b/web-view-components-compiler/src/test/lexer/tokens-files/componentValue_tokens.txt @@ -0,0 +1,18 @@ +0: ComponentOpen[1,1](<) +1: TypedIdentifier[1,2](Test) +2: Nlws[1,6]( ) +3: AttributeIdentifier[1,7](transform) +4: Equals[1,16](=) +5: ComponentAttrValueStart[1,17]({) +6: ComponentOpen[1,18](<) +7: StringIdentifier[1,19](p) +8: ComponentClose[1,20](>) +9: DollarReferenceStart[1,21]($) +10: GroovyCode[1,22](it) +11: ClosingComponentOpen[1,24]() +14: ComponentAttrValueEnd[1,28](}) +15: Nlws[1,29]( ) +16: ComponentSelfClose[1,30](/>) +17: RawText[1,32](\n) \ No newline at end of file diff --git a/web-view-components-compiler/src/test/parser/componentValue.wvc b/web-view-components-compiler/src/test/parser/componentValue.wvc new file mode 100644 index 0000000..8a686fc --- /dev/null +++ b/web-view-components-compiler/src/test/parser/componentValue.wvc @@ -0,0 +1 @@ +$it

} /> diff --git a/web-view-components-compiler/src/test/parser/componentValueNested.wvc b/web-view-components-compiler/src/test/parser/componentValueNested.wvc new file mode 100644 index 0000000..4758b5f --- /dev/null +++ b/web-view-components-compiler/src/test/parser/componentValueNested.wvc @@ -0,0 +1 @@ +} />} /> diff --git a/web-view-components-compiler/src/test/parser/componentValueNestedP.wvc b/web-view-components-compiler/src/test/parser/componentValueNestedP.wvc new file mode 100644 index 0000000..d582a57 --- /dev/null +++ b/web-view-components-compiler/src/test/parser/componentValueNestedP.wvc @@ -0,0 +1 @@ +$it

} />} /> diff --git a/web-view-components-compiler/src/test/parser/parse-tree-files/componentValueNestedP_parseTree.txt b/web-view-components-compiler/src/test/parser/parse-tree-files/componentValueNestedP_parseTree.txt new file mode 100644 index 0000000..dc9dcff --- /dev/null +++ b/web-view-components-compiler/src/test/parser/parse-tree-files/componentValueNestedP_parseTree.txt @@ -0,0 +1,55 @@ +compilationUnit[1,1..2,1] + body[1,1..1,43] + component[1,1..1,43] + selfClosingComponent[1,1..1,43] + ComponentOpen[1,1](<) + componentArgs[1,2..1,39] + componentType[1,2..1,6] + TypedIdentifier[1,2](Test) + attr[1,7..1,39] + keyValueAttr[1,7..1,39] + AttributeIdentifier[1,7](test) + Equals[1,11](=) + value[1,12..1,39] + componentAttrValue[1,12..1,39] + ComponentAttrValueStart[1,12]({) + component[1,13..1,39] + selfClosingComponent[1,13..1,39] + ComponentOpen[1,13](<) + componentArgs[1,14..1,35] + componentType[1,14..1,18] + TypedIdentifier[1,14](Test) + attr[1,19..1,35] + keyValueAttr[1,19..1,35] + AttributeIdentifier[1,19](test) + Equals[1,23](=) + value[1,24..1,35] + componentAttrValue[1,24..1,35] + ComponentAttrValueStart[1,24]({) + component[1,25..1,34] + componentWithChildren[1,25..1,34] + openComponent[1,25..1,27] + ComponentOpen[1,25](<) + componentArgs[1,26..1,26] + componentType[1,26..1,26] + StringIdentifier[1,26](p) + ComponentClose[1,27](>) + body[1,28..1,31] + bodyText[1,28..1,31] + bodyTextGroovyElement[1,28..1,31] + dollarReference[1,28..1,31] + DollarReferenceStart[1,28]($) + GroovyCode[1,29](it) + closingComponent[1,31..1,34] + ClosingComponentOpen[1,31]() + ComponentAttrValueEnd[1,35](}) + ComponentSelfClose[1,37](/>) + ComponentAttrValueEnd[1,39](}) + ComponentSelfClose[1,41](/>) + bodyText[1,43..1,43] + text[1,43..1,43] + RawText[1,43](\n) + EOF[2,1]() diff --git a/web-view-components-compiler/src/test/parser/parse-tree-files/componentValueNested_parseTree.txt b/web-view-components-compiler/src/test/parser/parse-tree-files/componentValueNested_parseTree.txt new file mode 100644 index 0000000..512c8f0 --- /dev/null +++ b/web-view-components-compiler/src/test/parser/parse-tree-files/componentValueNested_parseTree.txt @@ -0,0 +1,43 @@ +compilationUnit[1,1..2,1] + body[1,1..1,41] + component[1,1..1,41] + selfClosingComponent[1,1..1,41] + ComponentOpen[1,1](<) + componentArgs[1,2..1,37] + componentType[1,2..1,6] + TypedIdentifier[1,2](Test) + attr[1,7..1,37] + keyValueAttr[1,7..1,37] + AttributeIdentifier[1,7](test) + Equals[1,11](=) + value[1,12..1,37] + componentAttrValue[1,12..1,37] + ComponentAttrValueStart[1,12]({) + component[1,13..1,37] + selfClosingComponent[1,13..1,37] + ComponentOpen[1,13](<) + componentArgs[1,14..1,33] + componentType[1,14..1,18] + TypedIdentifier[1,14](Test) + attr[1,19..1,33] + keyValueAttr[1,19..1,33] + AttributeIdentifier[1,19](test) + Equals[1,23](=) + value[1,24..1,33] + componentAttrValue[1,24..1,33] + ComponentAttrValueStart[1,24]({) + component[1,25..1,33] + selfClosingComponent[1,25..1,33] + ComponentOpen[1,25](<) + componentArgs[1,26..1,30] + componentType[1,26..1,30] + TypedIdentifier[1,26](Test) + ComponentSelfClose[1,31](/>) + ComponentAttrValueEnd[1,33](}) + ComponentSelfClose[1,35](/>) + ComponentAttrValueEnd[1,37](}) + ComponentSelfClose[1,39](/>) + bodyText[1,41..1,41] + text[1,41..1,41] + RawText[1,41](\n) + EOF[2,1]() diff --git a/web-view-components-compiler/src/test/parser/parse-tree-files/componentValue_parseTree.txt b/web-view-components-compiler/src/test/parser/parse-tree-files/componentValue_parseTree.txt new file mode 100644 index 0000000..5632c8e --- /dev/null +++ b/web-view-components-compiler/src/test/parser/parse-tree-files/componentValue_parseTree.txt @@ -0,0 +1,40 @@ +compilationUnit[1,1..2,1] + body[1,1..1,27] + component[1,1..1,27] + selfClosingComponent[1,1..1,27] + ComponentOpen[1,1](<) + componentArgs[1,2..1,23] + componentType[1,2..1,6] + TypedIdentifier[1,2](Test) + attr[1,7..1,23] + keyValueAttr[1,7..1,23] + AttributeIdentifier[1,7](test) + Equals[1,11](=) + value[1,12..1,23] + componentAttrValue[1,12..1,23] + ComponentAttrValueStart[1,12]({) + component[1,13..1,22] + componentWithChildren[1,13..1,22] + openComponent[1,13..1,15] + ComponentOpen[1,13](<) + componentArgs[1,14..1,14] + componentType[1,14..1,14] + StringIdentifier[1,14](p) + ComponentClose[1,15](>) + body[1,16..1,19] + bodyText[1,16..1,19] + bodyTextGroovyElement[1,16..1,19] + dollarReference[1,16..1,19] + DollarReferenceStart[1,16]($) + GroovyCode[1,17](it) + closingComponent[1,19..1,22] + ClosingComponentOpen[1,19]() + ComponentAttrValueEnd[1,23](}) + ComponentSelfClose[1,25](/>) + bodyText[1,27..1,27] + text[1,27..1,27] + RawText[1,27](\n) + EOF[2,1]()