From a4bfea92efcf872f86087d339822f3ad6ac4cf5d Mon Sep 17 00:00:00 2001 From: JesseBrault0709 <62299747+JesseBrault0709@users.noreply.github.com> Date: Sun, 22 Jan 2023 12:51:54 -0600 Subject: [PATCH] component node --- .../ssg/template/gspe/ComponentFactory.groovy | 5 - .../ssg/template/gspe/ComponentParser.groovy | 256 ------------------ .../ssg/template/gspe/GspeTemplate.groovy | 1 + .../template/gspe/GspeTemplateEngine.groovy | 3 +- .../ssg/template/gspe/PatternFunction.groovy | 1 - .../gspe/{ => component}/Component.groovy | 2 +- .../gspe/component/ComponentFactory.groovy | 5 + .../gspe/component/ComponentParser.groovy | 224 +++++++++++++++ .../component/ComponentToScriptVisitor.groovy | 83 ++++++ .../{ => component}/ComponentToken.groovy | 4 +- .../{ => component}/ComponentTokenizer.groovy | 3 +- .../ComponentsContainer.groovy | 2 +- .../gspe/{ => component}/PeekBefore.groovy | 2 +- .../gspe/component/node/BooleanValue.groovy | 11 + .../gspe/component/node/ComponentNode.groovy | 13 + .../node/DollarReferenceValue.groovy | 12 + .../node/DollarScriptletValue.groovy | 12 + .../node/ExpressionScriptletValue.groovy | 12 + .../gspe/component/node/GStringValue.groovy | 12 + .../gspe/component/node/KeyAndValue.groovy | 12 + .../gspe/component/node/KeysAndValues.groovy | 10 + .../template/gspe/component/node/Node.groovy | 5 + .../gspe/component/node/NodeVisitor.groovy | 15 + .../gspe/component/node/ScriptletValue.groovy | 12 + .../gspe/component/node/StringValue.groovy | 12 + .../gspe/GspeTemplateEngineTests.groovy | 2 +- .../component/ComponentParserTests.groovy | 140 ++++++++++ .../ComponentTokenizerTests.groovy | 5 +- .../test/resources/components/Greeting.groovy | 2 +- .../src/test/resources/components/Head.groovy | 2 +- 30 files changed, 606 insertions(+), 274 deletions(-) delete mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentFactory.groovy delete mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentParser.groovy rename template/src/main/groovy/com/jessebrault/ssg/template/gspe/{ => component}/Component.groovy (58%) create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentFactory.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentParser.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentToScriptVisitor.groovy rename template/src/main/groovy/com/jessebrault/ssg/template/gspe/{ => component}/ComponentToken.groovy (88%) rename template/src/main/groovy/com/jessebrault/ssg/template/gspe/{ => component}/ComponentTokenizer.groovy (98%) rename template/src/main/groovy/com/jessebrault/ssg/template/gspe/{ => component}/ComponentsContainer.groovy (96%) rename template/src/main/groovy/com/jessebrault/ssg/template/gspe/{ => component}/PeekBefore.groovy (77%) create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/BooleanValue.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/ComponentNode.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/DollarReferenceValue.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/DollarScriptletValue.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/ExpressionScriptletValue.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/GStringValue.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/KeyAndValue.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/KeysAndValues.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/Node.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/NodeVisitor.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/ScriptletValue.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/StringValue.groovy create mode 100644 template/src/test/groovy/com/jessebrault/ssg/template/gspe/component/ComponentParserTests.groovy rename template/src/test/groovy/com/jessebrault/ssg/template/gspe/{ => component}/ComponentTokenizerTests.groovy (96%) diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentFactory.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentFactory.groovy deleted file mode 100644 index e364454..0000000 --- a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentFactory.groovy +++ /dev/null @@ -1,5 +0,0 @@ -package com.jessebrault.ssg.template.gspe - -interface ComponentFactory { - Component get() -} \ No newline at end of file diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentParser.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentParser.groovy deleted file mode 100644 index 9e85d3b..0000000 --- a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentParser.groovy +++ /dev/null @@ -1,256 +0,0 @@ -package com.jessebrault.ssg.template.gspe - - -import groovy.transform.PackageScope - -import static com.jessebrault.ssg.template.gspe.ComponentToken.Type.* - -/** - * NOT thread safe - */ -@PackageScope -class ComponentParser { - - private Queue tokens - private StringBuilder b - - private String identifier - - String parse(List tokens) { - this.b = new StringBuilder() - this.tokens = new LinkedList<>(tokens) - - this.selfClosingComponent() - - this.b.toString() - } - - String parse(List openingTokens, String bodyClosure, List closingTokens) { - this.b = new StringBuilder() - - this.tokens = new LinkedList<>(openingTokens) - this.openingComponent() - - this.b << "bodyOut << ${ bodyClosure };" - - this.tokens = new LinkedList<>(closingTokens) - this.closingComponent() - - this.b.toString() - } - - private static void error(Collection expectedTypes, ComponentToken actual) { - throw new RuntimeException("expected ${ expectedTypes.join(' or ') } but got ${ actual ? "'${ actual }'" : 'null' }") - } - - private void selfClosingComponent() { - this.startOfOpeningOrSelfClosingComponent() - this.keysAndValues() - def t0 = this.tokens.poll() - if (!t0 || t0.type != FORWARD_SLASH) { - error([FORWARD_SLASH], t0) - } else { - def t1 = tokens.poll() - if (!t1 || t1.type != GT) { - error([GT], t1) - } else { - this.b << '};' - } - } - } - - private void openingComponent() { - this.startOfOpeningOrSelfClosingComponent() - this.keysAndValues() - def t0 = tokens.poll() - if (!t0 || t0.type != GT) { - error([GT], t0) - } - } - - private void closingComponent() { - def t0 = this.tokens.poll() - if (!t0 || t0.type != LT) { - error([LT], t0) - } else { - def t1 = this.tokens.poll() - if (!t1 || t1.type != FORWARD_SLASH) { - error([FORWARD_SLASH], t1) - } else { - def t2 = this.tokens.poll() - if (!t2 || t2.type != IDENTIFIER) { - error([IDENTIFIER], t2) - } else if (t2.text != identifier) { - throw new RuntimeException("expected '${ this.identifier }' but got '${ t2.text }'") - } else { - def t3 = this.tokens.poll() - if (!t3 || t3.type != GT) { - error([GT], t3) - } else { - this.b << '};' - } - } - } - } - } - - private void startOfOpeningOrSelfClosingComponent() { - def t0 = this.tokens.poll() - if (!t0 || t0.type != LT) { - error([LT], t0) - } else { - def t1 = this.tokens.poll() - if (!t1 || t1.type != IDENTIFIER) { - error([IDENTIFIER], t1) - } else { - this.identifier = t1.text - this.b << "renderComponent('${ this.identifier }') { attr, bodyOut ->\n" - } - } - } - - private void keysAndValues() { - while (true) { - def t0 = this.tokens.peek() - if (!t0 || !t0.type.isAnyOf([KEY, FORWARD_SLASH])) { - error([KEY, FORWARD_SLASH], t0) - } else if (t0.type == KEY) { - this.keyAndValue() - } else if (t0.type == FORWARD_SLASH) { - break - } - } - } - - @PeekBefore(KEY) - private void keyAndValue() { - def t0 = this.tokens.remove() - if (t0.type != KEY) { - throw new RuntimeException('programmer error') - } else { - def t1 = this.tokens.poll() - if (!t1 || t1.type != EQUALS) { - error([EQUALS], t1) - } else { - this.b << "attr['${ t0.text }'] = " - this.value() - } - } - } - - private void value() { - def t0 = this.tokens.peek() - if (!t0 || !t0.type.isAnyOf([DOUBLE_QUOTE, SINGLE_QUOTE, DOLLAR, LT])) { - error([DOUBLE_QUOTE, SINGLE_QUOTE, DOLLAR, LT], t0) - } else if (t0.type == DOUBLE_QUOTE) { - this.doubleQuoteStringValue() - } else if (t0.type == SINGLE_QUOTE) { - this.singleQuoteStringValue() - } else if (t0.type == DOLLAR) { - this.dollarValue() - } else if (t0.type == LT) { - this.scriptletValue() - } - } - - @PeekBefore(DOUBLE_QUOTE) - private void doubleQuoteStringValue() { - def t0 = this.tokens.remove() - if (t0.type != DOUBLE_QUOTE) { - throw new RuntimeException('programmer error') - } else { - def t1 = this.tokens.poll() - if (!t1 || t1.type != STRING) { - error([STRING], t1) - } else { - def t2 = this.tokens.poll() - if (!t2 || t2.type != DOUBLE_QUOTE) { - error([DOUBLE_QUOTE], t2) - } else { - this.b << /"${ t1.text }";/ + '\n' - } - } - } - } - - @PeekBefore(SINGLE_QUOTE) - private void singleQuoteStringValue() { - def t0 = this.tokens.remove() - if (t0.type != SINGLE_QUOTE) { - throw new RuntimeException('programmer error') - } else { - def t1 = tokens.poll() - if (!t1 || t1.type != STRING) { - error([STRING], t1) - } else { - def t2 = this.tokens.poll() - if (!t2 || t2.type != SINGLE_QUOTE) { - error([SINGLE_QUOTE], t2) - } else { - this.b << /'${ t1.text }';/ + '\n' - } - } - } - } - - @PeekBefore(DOLLAR) - private void dollarValue() { - def t0 = this.tokens.remove() - if (t0.type != DOLLAR) { - throw new RuntimeException('programmer error') - } else { - def t1 = this.tokens.poll() - if (!t1 || t1.type != CURLY_OPEN) { - error([CURLY_OPEN], t1) - } else { - def t2 = this.tokens.poll() - if (!t2 || t2.type != GROOVY) { - error([GROOVY], t2) - } else { - def t3 = this.tokens.poll() - if (!t3 || t3.type != CURLY_CLOSE) { - error([CURLY_CLOSE], t3) - } else { - this.b << "${ t2.text };\n" - } - } - } - } - } - - @PeekBefore(LT) - private void scriptletValue() { - def t0 = this.tokens.remove() - if (t0.type != LT) { - throw new RuntimeException('programmer error') - } else { - def t1 = this.tokens.poll() - if (!t1 || t1.type != PERCENT) { - error([PERCENT], t1) - } else { - def t2 = this.tokens.poll() - if (!t2 || t2.type != EQUALS) { - error([EQUALS], t2) - } else { - def t3 = this.tokens.poll() - if (!t3 || t3.type != GROOVY) { - error([GROOVY], t3) - } else { - def t4 = this.tokens.poll() - if (!t4.type || t4.type != PERCENT) { - error([PERCENT], t4) - } else { - def t5 = this.tokens.poll() - if (!t5 || t5.type != GT) { - error([GT], t5) - } else { - this.b << "${ t3.text };\n" - } - } - } - } - } - } - } - -} diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/GspeTemplate.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/GspeTemplate.groovy index 8b0404f..f35469f 100644 --- a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/GspeTemplate.groovy +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/GspeTemplate.groovy @@ -1,5 +1,6 @@ package com.jessebrault.ssg.template.gspe +import com.jessebrault.ssg.template.gspe.component.ComponentsContainer import groovy.text.Template class GspeTemplate implements Template { diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/GspeTemplateEngine.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/GspeTemplateEngine.groovy index 7071c9c..42d8b7f 100644 --- a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/GspeTemplateEngine.groovy +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/GspeTemplateEngine.groovy @@ -1,6 +1,7 @@ package com.jessebrault.ssg.template.gspe - +import com.jessebrault.ssg.template.gspe.component.Component +import com.jessebrault.ssg.template.gspe.component.ComponentsContainer import groovy.text.Template import groovy.text.TemplateEngine import groovy.transform.TupleConstructor diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/PatternFunction.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/PatternFunction.groovy index fac8fc6..f8f6b5b 100644 --- a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/PatternFunction.groovy +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/PatternFunction.groovy @@ -6,7 +6,6 @@ import groovy.transform.TupleConstructor import java.util.function.Function import java.util.regex.Pattern -@PackageScope @TupleConstructor(includeFields = true, defaults = false) class PatternFunction implements Function { diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/Component.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/Component.groovy similarity index 58% rename from template/src/main/groovy/com/jessebrault/ssg/template/gspe/Component.groovy rename to template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/Component.groovy index 6c39620..71df3ff 100644 --- a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/Component.groovy +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/Component.groovy @@ -1,4 +1,4 @@ -package com.jessebrault.ssg.template.gspe +package com.jessebrault.ssg.template.gspe.component interface Component { String render(Map attr, String body) diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentFactory.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentFactory.groovy new file mode 100644 index 0000000..6aa8527 --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentFactory.groovy @@ -0,0 +1,5 @@ +package com.jessebrault.ssg.template.gspe.component + +interface ComponentFactory { + Component get() +} \ No newline at end of file diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentParser.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentParser.groovy new file mode 100644 index 0000000..9284935 --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentParser.groovy @@ -0,0 +1,224 @@ +package com.jessebrault.ssg.template.gspe.component + +import com.jessebrault.ssg.template.gspe.component.ComponentToken.Type +import com.jessebrault.ssg.template.gspe.component.node.ComponentNode +import com.jessebrault.ssg.template.gspe.component.node.DollarReferenceValue +import com.jessebrault.ssg.template.gspe.component.node.DollarScriptletValue +import com.jessebrault.ssg.template.gspe.component.node.ExpressionScriptletValue +import com.jessebrault.ssg.template.gspe.component.node.GStringValue +import com.jessebrault.ssg.template.gspe.component.node.KeyAndValue +import com.jessebrault.ssg.template.gspe.component.node.KeysAndValues +import com.jessebrault.ssg.template.gspe.component.node.Node +import com.jessebrault.ssg.template.gspe.component.node.ScriptletValue +import com.jessebrault.ssg.template.gspe.component.node.StringValue +import groovy.transform.PackageScope + +import static com.jessebrault.ssg.template.gspe.component.ComponentToken.Type.* + +/** + * NOT thread safe + */ +@PackageScope +class ComponentParser { + + private Queue tokens + private String currentIdentifier + + ComponentNode parse(Queue tokens) { + this.tokens = tokens + this.selfClosingComponent() + } + + String parse(Queue openingTokens, String bodyClosure, Queue closingTokens) { + this.tokens = openingTokens + def componentNode = this.openingComponent() + + componentNode.body = bodyClosure + + this.tokens = closingTokens + this.closingComponent() + + componentNode + } + + private static void error(Collection expectedTypes, ComponentToken actual) { + throw new RuntimeException("expected ${ expectedTypes.join(' or ') } but got ${ actual ? "'${ actual }'" : 'null' }") + } + + private ComponentToken expect(Collection types) { + def t = this.tokens.poll() + if (!t || !t.type.isAnyOf(types)) { + error(types, t) + } + t + } + + private ComponentToken expect(Type type) { + this.expect([type]) + } + + private boolean peek(Type type) { + def t = this.tokens.peek() + t && t.type == type + } + + private boolean peekSecond(Type type) { + def t = this.tokens[1] + t && t.type == type + } + + private boolean peekThird(Type type) { + def t = this.tokens[2] + t && t.type == type + } + + private ComponentNode selfClosingComponent() { + this.startOfOpeningOrSelfClosingComponent() + def keysAndValues = this.keysAndValues() + this.expect(FORWARD_SLASH) + this.expect(GT) + new ComponentNode().tap { + it.identifier = this.currentIdentifier + it.children << keysAndValues + } + } + + private ComponentNode openingComponent() { + this.startOfOpeningOrSelfClosingComponent() + def keysAndValues = this.keysAndValues() + this.expect(GT) + new ComponentNode().tap { + it.identifier = this.currentIdentifier + it.children << keysAndValues + } + } + + private void closingComponent() { + this.expect(LT) + this.expect(FORWARD_SLASH) + def identifierToken = this.expect(IDENTIFIER) + if (identifierToken.text != this.currentIdentifier) { + throw new RuntimeException("expected '${ this.currentIdentifier }' but got '${ t2.text }'") + } + this.expect(GT) + } + + private void startOfOpeningOrSelfClosingComponent() { + this.expect(LT) + def identifierToken = this.expect(IDENTIFIER) + this.currentIdentifier = identifierToken.text + } + + private KeysAndValues keysAndValues() { + List children = [] + while (true) { + if (this.peek(KEY)) { + def keyAndValue = this.keyAndValue() + children << keyAndValue + } else if (this.peek(FORWARD_SLASH)) { + break + } else { + error([KEY, FORWARD_SLASH], this.tokens.poll()) + } + } + new KeysAndValues().tap { + it.children.addAll(children) + } + } + + @PeekBefore(KEY) + private KeyAndValue keyAndValue() { + def keyToken = this.expect(KEY) + this.expect(EQUALS) + def value = this.value() + new KeyAndValue().tap { + key = keyToken.text + it.children << value + } + } + + private Node value() { + if (this.peek(DOUBLE_QUOTE)) { + return this.doubleQuoteStringValue() + } else if (this.peek(SINGLE_QUOTE)) { + return this.singleQuoteStringValue() + } else if (this.peek(DOLLAR) && this.peekSecond(GROOVY_IDENTIFIER)) { + return this.dollarReferenceValue() + } else if (this.peek(DOLLAR) && this.peekSecond(CURLY_OPEN)) { + return this.dollarScriptletValue() + } else if (this.peek(LT) && this.peekSecond(PERCENT) && this.peekThird(EQUALS)) { + return this.expressionScriptletValue() + } else if (this.peek(LT) && this.peekSecond(PERCENT)) { + return this.scriptletValue() + } else { + error([DOUBLE_QUOTE, SINGLE_QUOTE, DOLLAR, LT], this.tokens.poll()) + } + throw new RuntimeException('should not get here') + } + + @PeekBefore(DOUBLE_QUOTE) + private GStringValue doubleQuoteStringValue() { + this.expect(DOUBLE_QUOTE) + def stringToken = this.expect(STRING) + this.expect(DOUBLE_QUOTE) + new GStringValue().tap { + gString = stringToken.text + } + } + + @PeekBefore(SINGLE_QUOTE) + private StringValue singleQuoteStringValue() { + this.expect(SINGLE_QUOTE) + def stringToken = this.expect(STRING) + this.expect(SINGLE_QUOTE) + new StringValue().tap { + string = stringToken.text + } + } + + @PeekBefore([DOLLAR, GROOVY_IDENTIFIER]) + private DollarReferenceValue dollarReferenceValue() { + this.expect(DOLLAR) + def groovyIdentifierToken = this.expect(GROOVY_IDENTIFIER) + new DollarReferenceValue().tap { + reference = groovyIdentifierToken.text + } + } + + @PeekBefore([DOLLAR, CURLY_OPEN]) + private DollarScriptletValue dollarScriptletValue() { + this.expect(DOLLAR) + this.expect(CURLY_OPEN) + def groovyToken = this.expect(GROOVY) + this.expect(CURLY_CLOSE) + new DollarScriptletValue().tap { + scriptlet = groovyToken.text + } + } + + @PeekBefore([LT, PERCENT, EQUALS]) + private ExpressionScriptletValue expressionScriptletValue() { + this.expect(LT) + this.expect(PERCENT) + this.expect(EQUALS) + def groovyToken = this.expect(GROOVY) + this.expect(PERCENT) + this.expect(GT) + new ExpressionScriptletValue().tap { + scriptlet = groovyToken.text + } + } + + @PeekBefore([LT, PERCENT]) + private ScriptletValue scriptletValue() { + this.expect(LT) + this.expect(PERCENT) + def groovyToken = this.expect(GROOVY) + this.expect(PERCENT) + this.expect(GT) + new ScriptletValue().tap { + scriptlet = groovyToken.text + } + } + +} diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentToScriptVisitor.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentToScriptVisitor.groovy new file mode 100644 index 0000000..031e015 --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentToScriptVisitor.groovy @@ -0,0 +1,83 @@ +package com.jessebrault.ssg.template.gspe.component + +import com.jessebrault.ssg.template.gspe.component.node.BooleanValue +import com.jessebrault.ssg.template.gspe.component.node.ComponentNode +import com.jessebrault.ssg.template.gspe.component.node.DollarReferenceValue +import com.jessebrault.ssg.template.gspe.component.node.DollarScriptletValue +import com.jessebrault.ssg.template.gspe.component.node.ExpressionScriptletValue +import com.jessebrault.ssg.template.gspe.component.node.GStringValue +import com.jessebrault.ssg.template.gspe.component.node.KeyAndValue +import com.jessebrault.ssg.template.gspe.component.node.KeysAndValues +import com.jessebrault.ssg.template.gspe.component.node.NodeVisitor +import com.jessebrault.ssg.template.gspe.component.node.ScriptletValue +import com.jessebrault.ssg.template.gspe.component.node.StringValue + +// NOT THREAD SAFE, and must be used exactly once +class ComponentToScriptVisitor extends NodeVisitor { + + private final Writer w = new StringWriter() + private final IndentPrinter p = new IndentPrinter(this.w, ' ', true, true) + + String getResult() { + w.toString() + } + + void visit(ComponentNode componentNode) { + p.println('{ attr, bodyOut ->') + p.incrementIndent() + super.visit(componentNode) + if (componentNode.body != null) { + p.println("bodyOut << ${ componentNode.body };") + } + p.decrementIndent() + p.println('};') + } + + void visit(KeysAndValues keysAndValues) { + p.println('attr {') + p.incrementIndent() + super.visit(keysAndValues) + p.decrementIndent() + p.println('};') + } + + void visit(KeyAndValue keyAndValue) { + p.printIndent() + p.print("${ keyAndValue.key } = ") + super.visit(keyAndValue) + p.print(';\n') + } + + void visit(GStringValue gStringValue) { + p.print("\"${ gStringValue.gString }\"") + } + + void visit(StringValue stringValue) { + p.print("'${ stringValue.string }'") + } + + void visit(DollarReferenceValue dollarReferenceValue) { + p.print(dollarReferenceValue.reference) + } + + void visit(DollarScriptletValue dollarScriptletValue) { + p.print(dollarScriptletValue.scriptlet) + } + + void visit(ScriptletValue scriptletValue) { + p.println("render { out ->") + p.incrementIndent() + p.print(scriptletValue.scriptlet) + p.decrementIndent() + p.println('}') + } + + void visit(ExpressionScriptletValue expressionScriptletValue) { + p.print(expressionScriptletValue.scriptlet) + } + + void visit(BooleanValue booleanValue) { + p.print(booleanValue.value.toString()) + } + +} diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentToken.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentToken.groovy similarity index 88% rename from template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentToken.groovy rename to template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentToken.groovy index f182cc3..9caeed7 100644 --- a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentToken.groovy +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentToken.groovy @@ -1,8 +1,8 @@ -package com.jessebrault.ssg.template.gspe +package com.jessebrault.ssg.template.gspe.component import groovy.transform.TupleConstructor -@TupleConstructor(defaults = false) +@TupleConstructor class ComponentToken { enum Type { diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentTokenizer.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentTokenizer.groovy similarity index 98% rename from template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentTokenizer.groovy rename to template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentTokenizer.groovy index 9ff23b1..0579df8 100644 --- a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentTokenizer.groovy +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentTokenizer.groovy @@ -1,7 +1,8 @@ -package com.jessebrault.ssg.template.gspe +package com.jessebrault.ssg.template.gspe.component import com.jessebrault.fsm.function.FunctionFsmBuilder import com.jessebrault.fsm.function.FunctionFsmBuilderImpl +import com.jessebrault.ssg.template.gspe.PatternFunction import static ComponentToken.Type diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentsContainer.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentsContainer.groovy similarity index 96% rename from template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentsContainer.groovy rename to template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentsContainer.groovy index ae86b1a..6eb56f3 100644 --- a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentsContainer.groovy +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/ComponentsContainer.groovy @@ -1,4 +1,4 @@ -package com.jessebrault.ssg.template.gspe +package com.jessebrault.ssg.template.gspe.component import org.slf4j.Logger import org.slf4j.LoggerFactory diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/PeekBefore.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/PeekBefore.groovy similarity index 77% rename from template/src/main/groovy/com/jessebrault/ssg/template/gspe/PeekBefore.groovy rename to template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/PeekBefore.groovy index 611dd0e..62a1d18 100644 --- a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/PeekBefore.groovy +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/PeekBefore.groovy @@ -1,4 +1,4 @@ -package com.jessebrault.ssg.template.gspe +package com.jessebrault.ssg.template.gspe.component import java.lang.annotation.Retention diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/BooleanValue.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/BooleanValue.groovy new file mode 100644 index 0000000..c54e379 --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/BooleanValue.groovy @@ -0,0 +1,11 @@ +package com.jessebrault.ssg.template.gspe.component.node + +class BooleanValue extends Node { + boolean value + + @Override + String toString() { + "BooleanValue(${ this.value })" + } + +} diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/ComponentNode.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/ComponentNode.groovy new file mode 100644 index 0000000..d680826 --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/ComponentNode.groovy @@ -0,0 +1,13 @@ +package com.jessebrault.ssg.template.gspe.component.node + +class ComponentNode extends Node { + + String identifier + String body + + @Override + String toString() { + "ComponentNode(identifier: ${ this.identifier }, body: ${ this.body }, children: ${ this.children })" + } + +} diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/DollarReferenceValue.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/DollarReferenceValue.groovy new file mode 100644 index 0000000..65938d7 --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/DollarReferenceValue.groovy @@ -0,0 +1,12 @@ +package com.jessebrault.ssg.template.gspe.component.node + +class DollarReferenceValue extends Node { + + String reference + + @Override + String toString() { + "DollarReferenceValue(${ this.reference })" + } + +} diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/DollarScriptletValue.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/DollarScriptletValue.groovy new file mode 100644 index 0000000..a7308f6 --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/DollarScriptletValue.groovy @@ -0,0 +1,12 @@ +package com.jessebrault.ssg.template.gspe.component.node + +class DollarScriptletValue extends Node { + + String scriptlet + + @Override + String toString() { + "DollarScriptletValue(${ this.scriptlet })" + } + +} diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/ExpressionScriptletValue.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/ExpressionScriptletValue.groovy new file mode 100644 index 0000000..362044a --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/ExpressionScriptletValue.groovy @@ -0,0 +1,12 @@ +package com.jessebrault.ssg.template.gspe.component.node + +class ExpressionScriptletValue extends Node { + + String scriptlet + + @Override + String toString() { + "ExpressionScriptletValue(${ this.scriptlet })" + } + +} diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/GStringValue.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/GStringValue.groovy new file mode 100644 index 0000000..a474ad4 --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/GStringValue.groovy @@ -0,0 +1,12 @@ +package com.jessebrault.ssg.template.gspe.component.node + +class GStringValue extends Node { + + String gString + + @Override + String toString() { + "GStringValue(${ this.gString })" + } + +} diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/KeyAndValue.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/KeyAndValue.groovy new file mode 100644 index 0000000..4b2c2d5 --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/KeyAndValue.groovy @@ -0,0 +1,12 @@ +package com.jessebrault.ssg.template.gspe.component.node + +class KeyAndValue extends Node { + + String key + + @Override + String toString() { + "KeyAndValue(key: ${ this.key }, children: ${ this.children })" + } + +} diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/KeysAndValues.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/KeysAndValues.groovy new file mode 100644 index 0000000..2033816 --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/KeysAndValues.groovy @@ -0,0 +1,10 @@ +package com.jessebrault.ssg.template.gspe.component.node + +class KeysAndValues extends Node { + + @Override + String toString() { + "KeysAndValues(${ this.children })" + } + +} diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/Node.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/Node.groovy new file mode 100644 index 0000000..0567c8c --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/Node.groovy @@ -0,0 +1,5 @@ +package com.jessebrault.ssg.template.gspe.component.node + +abstract class Node { + List children = [] +} diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/NodeVisitor.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/NodeVisitor.groovy new file mode 100644 index 0000000..21d182a --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/NodeVisitor.groovy @@ -0,0 +1,15 @@ +package com.jessebrault.ssg.template.gspe.component.node + +abstract class NodeVisitor { + + void visit(Node node) { + this.visitChildren(node) + } + + void visitChildren(Node node) { + node.children.each { + this.visit(it) + } + } + +} diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/ScriptletValue.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/ScriptletValue.groovy new file mode 100644 index 0000000..acbd454 --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/ScriptletValue.groovy @@ -0,0 +1,12 @@ +package com.jessebrault.ssg.template.gspe.component.node + +class ScriptletValue extends Node { + + String scriptlet + + @Override + String toString() { + "ScriptletValue(${ this.scriptlet })" + } + +} diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/StringValue.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/StringValue.groovy new file mode 100644 index 0000000..dbcb156 --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/component/node/StringValue.groovy @@ -0,0 +1,12 @@ +package com.jessebrault.ssg.template.gspe.component.node + +class StringValue extends Node { + + String string + + @Override + String toString() { + "StringValue(${ this.string })" + } + +} diff --git a/template/src/test/groovy/com/jessebrault/ssg/template/gspe/GspeTemplateEngineTests.groovy b/template/src/test/groovy/com/jessebrault/ssg/template/gspe/GspeTemplateEngineTests.groovy index 0759de2..e099e40 100644 --- a/template/src/test/groovy/com/jessebrault/ssg/template/gspe/GspeTemplateEngineTests.groovy +++ b/template/src/test/groovy/com/jessebrault/ssg/template/gspe/GspeTemplateEngineTests.groovy @@ -1,6 +1,6 @@ package com.jessebrault.ssg.template.gspe - +import com.jessebrault.ssg.template.gspe.component.Component import groovy.text.TemplateEngine import org.junit.jupiter.api.Test diff --git a/template/src/test/groovy/com/jessebrault/ssg/template/gspe/component/ComponentParserTests.groovy b/template/src/test/groovy/com/jessebrault/ssg/template/gspe/component/ComponentParserTests.groovy new file mode 100644 index 0000000..1fc4658 --- /dev/null +++ b/template/src/test/groovy/com/jessebrault/ssg/template/gspe/component/ComponentParserTests.groovy @@ -0,0 +1,140 @@ +package com.jessebrault.ssg.template.gspe.component + +import com.jessebrault.ssg.template.gspe.component.node.ComponentNode +import com.jessebrault.ssg.template.gspe.component.node.GStringValue +import com.jessebrault.ssg.template.gspe.component.node.KeyAndValue +import com.jessebrault.ssg.template.gspe.component.node.KeysAndValues +import com.jessebrault.ssg.template.gspe.component.node.Node +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.FirstParam +import groovy.transform.stc.SimpleType +import org.junit.jupiter.api.Test +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import static com.jessebrault.ssg.template.gspe.component.ComponentToken.Type.* +import static org.junit.jupiter.api.Assertions.assertEquals +import static org.junit.jupiter.api.Assertions.assertTrue + +class ComponentParserTests { + + private static final Logger logger = LoggerFactory.getLogger(ComponentParserTests) + + private static class NodeSpec { + + Class nodeClass + Closure tests + + NodeSpec( + Class nodeClass, + @DelegatesTo(value = NodeTester, strategy = Closure.DELEGATE_FIRST) + @ClosureParams(FirstParam.FirstGenericType) + Closure tests = null + ) { + this.nodeClass = Objects.requireNonNull(nodeClass) + this.tests = tests + } + + void test(Node actual) { + logger.debug('actual: {}', actual) + assertTrue(nodeClass.isAssignableFrom(actual.class)) + if (this.tests != null) { + def nodeTester = new NodeTester() + this.tests.setDelegate(nodeTester) + this.tests.setResolveStrategy(Closure.DELEGATE_FIRST) + this.tests(actual) + + def childIterator = actual.children.iterator() + assertEquals(nodeTester.childSpecs.size(), actual.children.size()) + + nodeTester.childSpecs.each { + assertTrue(childIterator.hasNext()) + def next = childIterator.next() + it.test(next) + } + } + } + + @Override + String toString() { + "NodeSpec(${ this.nodeClass.simpleName })" + } + + } + + private static class NodeTester { + + List> childSpecs = [] + + def void expect( + Class childNodeClass, + @DelegatesTo(value = NodeTester, strategy = Closure.DELEGATE_ONLY) + @ClosureParams(FirstParam.FirstGenericType) + Closure furtherTests + ) { + this.childSpecs << new NodeSpec(childNodeClass, furtherTests) + } + + void expect(Class nodeClass) { + this.childSpecs << new NodeSpec(nodeClass) + } + + } + + private final ComponentParser parser = new ComponentParser() + + private void selfClosing( + List tokens, + @DelegatesTo(value = NodeTester, strategy = Closure.DELEGATE_FIRST) + @ClosureParams(value = SimpleType, options = ['com.jessebrault.ssg.template.gspe.component.node.ComponentNode']) + Closure tests + ) { + def componentNode = this.parser.parse(tokens) + logger.debug('componentNode: {}', componentNode) + + def componentSpec = new NodeSpec(ComponentNode, tests) + logger.debug('nodeSpec: {}', componentSpec) + componentSpec.test(componentNode) + } + + @Test + void selfClosingNoKeysOrValues() { + this.selfClosing([ + new ComponentToken(LT), + new ComponentToken(IDENTIFIER, 'Test'), + new ComponentToken(FORWARD_SLASH), + new ComponentToken(GT) + ]) { + assertEquals('Test', it.identifier) + expect(KeysAndValues) { + assertEquals(0, it.children.size()) + } + } + } + + @Test + void selfClosingWithGStringValue() { + this.selfClosing([ + new ComponentToken(LT), + new ComponentToken(IDENTIFIER, 'Test'), + new ComponentToken(KEY, 'test'), + new ComponentToken(EQUALS), + new ComponentToken(DOUBLE_QUOTE), + new ComponentToken(STRING, 'Hello, World!'), + new ComponentToken(DOUBLE_QUOTE), + new ComponentToken(FORWARD_SLASH), + new ComponentToken(GT) + ]) { + assertEquals('Test', it.identifier) + expect(KeysAndValues) { + expect(KeyAndValue) { + assertEquals('test', it.key) + expect(GStringValue) { + assertEquals('Hello, World!', it.gString) + } + } + } + } + } + +} diff --git a/template/src/test/groovy/com/jessebrault/ssg/template/gspe/ComponentTokenizerTests.groovy b/template/src/test/groovy/com/jessebrault/ssg/template/gspe/component/ComponentTokenizerTests.groovy similarity index 96% rename from template/src/test/groovy/com/jessebrault/ssg/template/gspe/ComponentTokenizerTests.groovy rename to template/src/test/groovy/com/jessebrault/ssg/template/gspe/component/ComponentTokenizerTests.groovy index 97a43c5..f52210c 100644 --- a/template/src/test/groovy/com/jessebrault/ssg/template/gspe/ComponentTokenizerTests.groovy +++ b/template/src/test/groovy/com/jessebrault/ssg/template/gspe/component/ComponentTokenizerTests.groovy @@ -1,10 +1,11 @@ -package com.jessebrault.ssg.template.gspe +package com.jessebrault.ssg.template.gspe.component + import org.junit.jupiter.api.Test import org.slf4j.Logger import org.slf4j.LoggerFactory -import static com.jessebrault.ssg.template.gspe.ComponentToken.Type.* +import static com.jessebrault.ssg.template.gspe.component.ComponentToken.Type.* import static org.junit.jupiter.api.Assertions.assertEquals import static org.junit.jupiter.api.Assertions.assertTrue diff --git a/template/src/test/resources/components/Greeting.groovy b/template/src/test/resources/components/Greeting.groovy index 78cdd69..0890e5b 100644 --- a/template/src/test/resources/components/Greeting.groovy +++ b/template/src/test/resources/components/Greeting.groovy @@ -1,6 +1,6 @@ package components -import com.jessebrault.ssg.template.gspe.Component +import com.jessebrault.ssg.template.gspe.component.Component class Greeting implements Component { diff --git a/template/src/test/resources/components/Head.groovy b/template/src/test/resources/components/Head.groovy index 67b6d03..b5c49ec 100644 --- a/template/src/test/resources/components/Head.groovy +++ b/template/src/test/resources/components/Head.groovy @@ -1,6 +1,6 @@ package components -import com.jessebrault.ssg.template.gspe.Component +import com.jessebrault.ssg.template.gspe.component.Component class Head implements Component {