From 8d37d8883d7777f6835b4b7b06aa19359fc0d47b Mon Sep 17 00:00:00 2001 From: JesseBrault0709 <62299747+JesseBrault0709@users.noreply.github.com> Date: Mon, 16 Jan 2023 20:05:10 -0600 Subject: [PATCH] Working on new Template --- build.gradle | 4 - buildSrc/build.gradle | 26 ++ buildSrc/src/main/groovy/ssg.common.gradle | 29 ++ buildSrc/src/main/groovy/ssg.lib.gradle | 33 +++ cli/build.gradle | 19 +- lib/build.gradle | 43 +-- settings.gradle | 2 +- template/build.gradle | 26 ++ .../ssg/template/gspe/Component.groovy | 5 + .../ssg/template/gspe/ComponentFactory.groovy | 5 + .../ssg/template/gspe/ComponentParser.groovy | 256 ++++++++++++++++++ .../ssg/template/gspe/ComponentToken.groovy | 35 +++ .../template/gspe/ComponentTokenizer.groovy | 176 ++++++++++++ .../template/gspe/ComponentsContainer.groovy | 43 +++ .../ssg/template/gspe/FsmState.groovy | 23 ++ .../ssg/template/gspe/GspeTemplate.groovy | 32 +++ .../template/gspe/GspeTemplateEngine.groovy | 56 ++++ .../ssg/template/gspe/PatternFunction.groovy | 26 ++ .../ssg/template/gspe/PeekBefore.groovy | 10 + .../gspe/TemplateToScriptConverter.groovy | 164 +++++++++++ .../gspe/ComponentTokenizerTests.groovy | 107 ++++++++ .../gspe/GspeTemplateEngineTests.groovy | 159 +++++++++++ .../test/resources/components/Greeting.groovy | 12 + .../src/test/resources/components/Head.groovy | 16 ++ template/src/test/resources/log4j2.xml | 17 ++ template/src/test/resources/test.ssg | 7 + 26 files changed, 1268 insertions(+), 63 deletions(-) delete mode 100644 build.gradle create mode 100644 buildSrc/build.gradle create mode 100644 buildSrc/src/main/groovy/ssg.common.gradle create mode 100644 buildSrc/src/main/groovy/ssg.lib.gradle create mode 100644 template/build.gradle create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/Component.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentFactory.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentParser.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentToken.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentTokenizer.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentsContainer.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/FsmState.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/GspeTemplate.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/GspeTemplateEngine.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/PatternFunction.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/PeekBefore.groovy create mode 100644 template/src/main/groovy/com/jessebrault/ssg/template/gspe/TemplateToScriptConverter.groovy create mode 100644 template/src/test/groovy/com/jessebrault/ssg/template/gspe/ComponentTokenizerTests.groovy create mode 100644 template/src/test/groovy/com/jessebrault/ssg/template/gspe/GspeTemplateEngineTests.groovy create mode 100644 template/src/test/resources/components/Greeting.groovy create mode 100644 template/src/test/resources/components/Head.groovy create mode 100644 template/src/test/resources/log4j2.xml create mode 100644 template/src/test/resources/test.ssg diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 07449b5..0000000 --- a/build.gradle +++ /dev/null @@ -1,4 +0,0 @@ -subprojects { - group = 'com.jessebrault.ssg' - version = '0.0.1' -} \ No newline at end of file diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 0000000..07ce7f9 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,26 @@ +plugins { + id 'groovy-gradle-plugin' +} + +repositories { + maven { + url 'https://archiva.jessebrault.com/repository/internal/' + + credentials { + username System.getenv('JBARCHIVA_USERNAME') + password System.getenv('JBARCHIVA_PASSWORD') + } + } + maven { + url 'https://archiva.jessebrault.com/repository/snapshots/' + + credentials { + username System.getenv('JBARCHIVA_USERNAME') + password System.getenv('JBARCHIVA_PASSWORD') + } + } +} + +dependencies { + implementation 'com.jessebrault.jbarchiva:jbarchiva:0.0.2' +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/ssg.common.gradle b/buildSrc/src/main/groovy/ssg.common.gradle new file mode 100644 index 0000000..5bbb160 --- /dev/null +++ b/buildSrc/src/main/groovy/ssg.common.gradle @@ -0,0 +1,29 @@ +plugins { + id 'com.jessebrault.jbarchiva' + id 'groovy' +} + +group 'com.jessebrault.ssg' +version '0.0.1' + +repositories { + mavenCentral() +} + +dependencies { + // https://mvnrepository.com/artifact/org.apache.groovy/groovy + implementation 'org.apache.groovy:groovy:4.0.7' + + /** + * TESTING + */ + // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + + // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' +} + +test { + useJUnitPlatform() +} diff --git a/buildSrc/src/main/groovy/ssg.lib.gradle b/buildSrc/src/main/groovy/ssg.lib.gradle new file mode 100644 index 0000000..443abeb --- /dev/null +++ b/buildSrc/src/main/groovy/ssg.lib.gradle @@ -0,0 +1,33 @@ +plugins { + id 'java-library' +} + +repositories { + mavenCentral() +} + +dependencies { + /** + * Logging + */ + // https://mvnrepository.com/artifact/org.slf4j/slf4j-api + implementation 'org.slf4j:slf4j-api:1.7.36' + + /** + * Test Logging + */ + // https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl + testRuntimeOnly 'org.apache.logging.log4j:log4j-slf4j-impl:2.19.0' + + // https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core + testRuntimeOnly 'org.apache.logging.log4j:log4j-core:2.19.0' + + /** + * Mockito + */ + // https://mvnrepository.com/artifact/org.mockito/mockito-core + testImplementation 'org.mockito:mockito-core:4.11.0' + + // https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter + testImplementation 'org.mockito:mockito-junit-jupiter:4.11.0' +} \ No newline at end of file diff --git a/cli/build.gradle b/cli/build.gradle index 4e93281..00d1833 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'groovy' + id 'ssg.common' id 'application' } @@ -10,9 +10,6 @@ repositories { dependencies { implementation project(':lib') - // https://mvnrepository.com/artifact/org.apache.groovy/groovy - implementation 'org.apache.groovy:groovy:4.0.7' - // https://mvnrepository.com/artifact/info.picocli/picocli implementation 'info.picocli:picocli:4.7.0' @@ -27,15 +24,6 @@ dependencies { // https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core implementation 'org.apache.logging.log4j:log4j-core:2.19.0' - - /** - * TESTING - */ - // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' - - // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' } application { @@ -49,10 +37,7 @@ jar { distributions { main { + //noinspection GroovyAssignabilityCheck distributionBaseName = 'ssg' } -} - -test { - useJUnitPlatform() } \ No newline at end of file diff --git a/lib/build.gradle b/lib/build.gradle index 0f23248..2c97aa3 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,5 +1,6 @@ plugins { - id 'groovy' + id 'ssg.common' + id 'ssg.lib' } repositories { @@ -7,9 +8,6 @@ repositories { } dependencies { - // https://mvnrepository.com/artifact/org.apache.groovy/groovy - implementation 'org.apache.groovy:groovy:4.0.7' - // https://mvnrepository.com/artifact/org.apache.groovy/groovy-templates implementation 'org.apache.groovy:groovy-templates:4.0.7' @@ -18,45 +16,8 @@ dependencies { // https://mvnrepository.com/artifact/org.commonmark/commonmark-ext-yaml-front-matter implementation 'org.commonmark:commonmark-ext-yaml-front-matter:0.21.0' - - /** - * Logging - */ - // https://mvnrepository.com/artifact/org.slf4j/slf4j-api - implementation 'org.slf4j:slf4j-api:1.7.36' - - /** - * TESTING - */ - // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' - - // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' - - /** - * Mockito - */ - // https://mvnrepository.com/artifact/org.mockito/mockito-core - testImplementation 'org.mockito:mockito-core:4.11.0' - - // https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter - testImplementation 'org.mockito:mockito-junit-jupiter:4.11.0' - - /** - * Test Logging - */ - // https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl - testRuntimeOnly 'org.apache.logging.log4j:log4j-slf4j-impl:2.19.0' - - // https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core - testRuntimeOnly 'org.apache.logging.log4j:log4j-core:2.19.0' } jar { archivesBaseName = 'ssg-lib' -} - -test { - useJUnitPlatform() } \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index d673b74..ebd2727 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ rootProject.name = 'ssg' -include 'cli', 'lib' \ No newline at end of file +include 'cli', 'lib', 'template' \ No newline at end of file diff --git a/template/build.gradle b/template/build.gradle new file mode 100644 index 0000000..af5cbf1 --- /dev/null +++ b/template/build.gradle @@ -0,0 +1,26 @@ +plugins { + id 'ssg.common' + id 'ssg.lib' +} + +repositories { + mavenCentral() +} + +dependencies { + // https://mvnrepository.com/artifact/org.apache.groovy/groovy + implementation 'org.apache.groovy:groovy:4.0.7' + + // https://mvnrepository.com/artifact/org.apache.groovy/groovy-templates + implementation 'org.apache.groovy:groovy-templates:4.0.7' + + // https://archiva.jessebrault.com/#artifact/com.jessebrault.fsm/lib/0.1.0-SNAPSHOT + implementation 'com.jessebrault.fsm:lib:0.1.0-SNAPSHOT' + + // https://archiva.jessebrault.com/#artifact/com.jessebrault.fsm/groovy-extension/0.1.0-SNAPSHOT + implementation 'com.jessebrault.fsm:groovy-extension:0.1.0-SNAPSHOT' +} + +jar { + archivesBaseName = 'ssg-template' +} \ No newline at end of file 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.groovy new file mode 100644 index 0000000..6c39620 --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/Component.groovy @@ -0,0 +1,5 @@ +package com.jessebrault.ssg.template.gspe + +interface Component { + String render(Map attr, String body) +} \ No newline at end of file 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 new file mode 100644 index 0000000..e364454 --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentFactory.groovy @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..9e85d3b --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentParser.groovy @@ -0,0 +1,256 @@ +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/ComponentToken.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentToken.groovy new file mode 100644 index 0000000..c93b321 --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentToken.groovy @@ -0,0 +1,35 @@ +package com.jessebrault.ssg.template.gspe + +import groovy.transform.TupleConstructor + +@TupleConstructor(defaults = false) +class ComponentToken { + + enum Type { + LT, + GT, + IDENTIFIER, + KEY, + EQUALS, + DOUBLE_QUOTE, + SINGLE_QUOTE, + STRING, + DOLLAR, + CURLY_OPEN, + GROOVY, + CURLY_CLOSE, + GROOVY_IDENTIFIER, + PERCENT, + FORWARD_SLASH + ; + + boolean isAnyOf(Collection types) { + types.contains(this) + } + + } + + Type type + String text + +} diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentTokenizer.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentTokenizer.groovy new file mode 100644 index 0000000..9bf60cb --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentTokenizer.groovy @@ -0,0 +1,176 @@ +package com.jessebrault.ssg.template.gspe + +import com.jessebrault.fsm.function.FunctionFsmBuilder +import com.jessebrault.fsm.function.FunctionFsmBuilderImpl + +import static ComponentToken.Type + +class ComponentTokenizer { + + private static final PatternFunction lessThan = new PatternFunction(~/^/) + private static final PatternFunction identifier = new PatternFunction(~/^\p{Lu}.*?(?=\s|\/)/) + private static final PatternFunction whitespace = new PatternFunction(~/^\s+/) + private static final PatternFunction key = new PatternFunction(~/^[\p{L}0-9_\-]+/) + private static final PatternFunction equals = new PatternFunction(~/^=/) + private static final PatternFunction doubleQuote = new PatternFunction(~/^"/) + private static final PatternFunction doubleQuoteStringContent = new PatternFunction(~/^(?:[\w\W&&[^\\"]]|\\")+/) + private static final PatternFunction singleQuote = new PatternFunction(~/^'/) + private static final PatternFunction singleQuoteStringContent = new PatternFunction(~/^(?:[\w\W&&[^\\']]|\\')+/) + + // https://docs.groovy-lang.org/latest/html/documentation/#_identifiers + //'\u00C0' to '\u00D6' + //'\u00D8' to '\u00F6' + //'\u00F8' to '\u00FF' + //'\u0100' to '\uFFFE' + private static final PatternFunction dollarReference = new PatternFunction(~/^\$[a-zA-Z_$\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\ufff3][a-zA-Z_$0-9\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\ufff3]*(?=[\s\/>])/) + + private static final PatternFunction dollarOpen = new PatternFunction(~/^\$\{/) + private static final PatternFunction percent = new PatternFunction(~/^%/) + private static final PatternFunction expressionScriptletGroovy = new PatternFunction(~/^.*?%>/) + private static final PatternFunction forwardSlash = new PatternFunction(~/^\//) + + static enum State { + START, + DOUBLE_QUOTE_STRING, + SINGLE_QUOTE_STRING, + DOLLAR_GROOVY, + EXPRESSION_SCRIPTLET_GROOVY, + DONE + } + + private static FunctionFsmBuilder getFsmBuilder() { + new FunctionFsmBuilderImpl<>() + } + + Queue tokenize(String src) { + Queue tokens = new LinkedList<>() + + def fsm = getFsmBuilder().with { + initialState = State.START + + whileIn(State.START) { + on lessThan exec { + tokens << new ComponentToken(Type.LT, it) + } + on greaterThan shiftTo State.DONE exec { + tokens << new ComponentToken(Type.GT, it) + } + on identifier exec { + tokens << new ComponentToken(Type.IDENTIFIER, it) + } + on whitespace exec { } + on key exec { + tokens << new ComponentToken(Type.KEY, it) + } + on equals exec { + tokens << new ComponentToken(Type.EQUALS, it) + } + on doubleQuote shiftTo State.DOUBLE_QUOTE_STRING exec { + tokens << new ComponentToken(Type.DOUBLE_QUOTE, it) + } + on singleQuote shiftTo State.SINGLE_QUOTE_STRING exec { + tokens << new ComponentToken(Type.SINGLE_QUOTE, it) + } + on dollarReference exec { String s -> + tokens << new ComponentToken(Type.DOLLAR, s[0]) + tokens << new ComponentToken(Type.GROOVY_IDENTIFIER, s.substring(1)) + } + on dollarOpen shiftTo State.DOLLAR_GROOVY exec { String s -> + tokens << new ComponentToken(Type.DOLLAR, s[0]) + tokens << new ComponentToken(Type.CURLY_OPEN, s[1]) + } + on percent shiftTo State.EXPRESSION_SCRIPTLET_GROOVY exec { + tokens << new ComponentToken(Type.PERCENT, it) + } + on forwardSlash exec { + tokens << new ComponentToken(Type.FORWARD_SLASH, it) + } + onNoMatch() exec { + throw new IllegalArgumentException() + } + } + + whileIn(State.DOUBLE_QUOTE_STRING) { + on doubleQuoteStringContent exec { + tokens << new ComponentToken(Type.STRING, it) + } + on doubleQuote shiftTo State.START exec { + tokens << new ComponentToken(Type.DOUBLE_QUOTE, it) + } + onNoMatch() exec { + throw new IllegalArgumentException() + } + } + + whileIn(State.SINGLE_QUOTE_STRING) { + on singleQuoteStringContent exec { + tokens << new ComponentToken(Type.STRING, it) + } + on singleQuote shiftTo State.START exec { + tokens << new ComponentToken(Type.SINGLE_QUOTE, it) + } + onNoMatch() exec { + throw new IllegalArgumentException() + } + } + + whileIn(State.DOLLAR_GROOVY) { + on { String input -> + def b = new StringBuilder() + def openCurlyCount = 1 + def iterator = input.iterator() as Iterator + while (iterator.hasNext()) { + def c0 = iterator.next() + if (c0 == '{') { + b << c0 + openCurlyCount++ + } else if (c0 == '}') { + b << c0 + openCurlyCount-- + if (openCurlyCount == 0) { + break + } + } else { + b << c0 + } + } + b.toString() + } shiftTo State.START exec { String s -> + tokens << new ComponentToken(Type.GROOVY, s.substring(0, s.length() - 1)) + tokens << new ComponentToken(Type.CURLY_CLOSE, '}') + } + onNoMatch() exec { + throw new IllegalArgumentException() + } + } + + whileIn(State.EXPRESSION_SCRIPTLET_GROOVY) { + on expressionScriptletGroovy shiftTo State.START exec { String s -> + tokens << new ComponentToken(Type.GROOVY, s.substring(0, s.length() - 2)) + tokens << new ComponentToken(Type.PERCENT, '%') + tokens << new ComponentToken(Type.GT, '>') + } + onNoMatch() exec { + throw new IllegalArgumentException() + } + } + + build() + } + + def remaining = src + + while (fsm.currentState != State.DONE) { + def output = fsm.apply(remaining) + if (!output) { + throw new IllegalStateException() + } else { + remaining = remaining.substring(output.length()) + } + } + + tokens + } + +} diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentsContainer.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentsContainer.groovy new file mode 100644 index 0000000..ae86b1a --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/ComponentsContainer.groovy @@ -0,0 +1,43 @@ +package com.jessebrault.ssg.template.gspe + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.slf4j.Marker +import org.slf4j.MarkerFactory + +class ComponentsContainer { + + private static final Logger logger = LoggerFactory.getLogger(ComponentsContainer) + private static final Marker enter = MarkerFactory.getMarker('ENTER') + private static final Marker exit = MarkerFactory.getMarker('EXIT') + + private final Map componentCache = [:] + private final GroovyClassLoader loader + + ComponentsContainer(Collection componentDirUrls, Collection components) { + logger.trace(enter, 'componentDirUrls: {}, components: {}', componentDirUrls, components) + this.loader = new GroovyClassLoader() + componentDirUrls.each { this.loader.addURL(it) } + components.each { + this.componentCache[it.class.simpleName] = it + } + logger.debug('this.loader: {}', this.loader) + logger.debug('this.componentCache: {}', this.componentCache) + logger.trace(exit, '') + } + + Component get(String name) { + logger.trace('name: {}', name) + def component = this.componentCache.computeIfAbsent(name, { + def componentClass = (Class) this.loader.loadClass(it) + componentClass.getDeclaredConstructor().newInstance() // must be a default constructor (for now) + }) + logger.trace(exit, 'component: {}', component) + component + } + + Component getAt(String name) { + this.get(name) + } + +} diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/FsmState.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/FsmState.groovy new file mode 100644 index 0000000..e8673de --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/FsmState.groovy @@ -0,0 +1,23 @@ +package com.jessebrault.ssg.template.gspe + +import groovy.transform.PackageScope + +@PackageScope +enum FsmState { + HTML, + SCRIPTLET, + EXPRESSION_SCRIPTLET, + DOLLAR, + + COMPONENT, + + COMPONENT_IDENTIFIER, + + COMPONENT_ATTR_KEY, + COMPONENT_ATTR_VALUE_OPEN, + + COMPONENT_ATTR_VALUE_STRING, + COMPONENT_ATTR_VALUE_STRING_CLOSE, + + COMPONENT_CLOSE +} 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 new file mode 100644 index 0000000..7b73464 --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/GspeTemplate.groovy @@ -0,0 +1,32 @@ +package com.jessebrault.ssg.template.gspe + + +import groovy.text.Template + +class GspeTemplate implements Template { + + Closure templateClosure + ComponentsContainer components + + String renderComponent(String componentName, Closure configureComponentInstance) { + Map attr = [:] + def bodyOut = new StringBuilder() + configureComponentInstance(attr, bodyOut) + + def component = this.components[componentName] + component.render(attr, bodyOut.toString()) + } + + @Override + final Writable make() { + this.make([:]) + } + + @Override + final Writable make(Map binding) { + def rehydrated = this.templateClosure.rehydrate(binding, this, this).asWritable() + rehydrated.setResolveStrategy(Closure.DELEGATE_FIRST) + rehydrated + } + +} \ No newline at end of file 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 new file mode 100644 index 0000000..7071c9c --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/GspeTemplateEngine.groovy @@ -0,0 +1,56 @@ +package com.jessebrault.ssg.template.gspe + + +import groovy.text.Template +import groovy.text.TemplateEngine +import groovy.transform.TupleConstructor +import org.codehaus.groovy.control.CompilationFailedException +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.util.concurrent.atomic.AtomicInteger +import java.util.function.Supplier + +final class GspeTemplateEngine extends TemplateEngine { + + private static final Logger logger = LoggerFactory.getLogger(GspeTemplateEngine) + + @TupleConstructor(defaults = false) + static class Configuration { + Supplier ssgTemplateSupplier + Collection componentDirUrls + Collection components + } + + private final Configuration configuration + private final File scriptsDir = File.createTempDir() + private final AtomicInteger templateCount = new AtomicInteger(0) + private final GroovyScriptEngine scriptEngine + + GspeTemplateEngine(Configuration configuration) { + this.configuration = configuration + this.scriptEngine = new GroovyScriptEngine([this.scriptsDir.toURI().toURL()] as URL[]) + } + + @Override + Template createTemplate(Reader reader) throws CompilationFailedException, ClassNotFoundException, IOException { + def templateSrc = reader.text + + def converter = new TemplateToScriptConverter() + def scriptSrc = converter.convert(templateSrc) + logger.debug('scriptSrc: {}', scriptSrc) + def scriptName = "SsgTemplate${ this.templateCount.getAndIncrement() }.groovy" + new File(this.scriptsDir, scriptName).write(scriptSrc) + + def script = this.scriptEngine.createScript(scriptName, new Binding()) + + def templateClosure = (Closure) script.invokeMethod('getTemplate', null) + + def template = this.configuration.ssgTemplateSupplier.get() + template.templateClosure = templateClosure + template.components = new ComponentsContainer(this.configuration.componentDirUrls, this.configuration.components) + + template + } + +} 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 new file mode 100644 index 0000000..fac8fc6 --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/PatternFunction.groovy @@ -0,0 +1,26 @@ +package com.jessebrault.ssg.template.gspe + +import groovy.transform.PackageScope +import groovy.transform.TupleConstructor + +import java.util.function.Function +import java.util.regex.Pattern + +@PackageScope +@TupleConstructor(includeFields = true, defaults = false) +class PatternFunction implements Function { + + protected final Pattern pattern + + @Override + String apply(String s) { + def matcher = this.pattern.matcher(s) + matcher.find() ? matcher.group() : null + } + + @Override + String toString() { + "PatternFunction(pattern: ${ this.pattern })" + } + +} diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/PeekBefore.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/PeekBefore.groovy new file mode 100644 index 0000000..611dd0e --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/PeekBefore.groovy @@ -0,0 +1,10 @@ +package com.jessebrault.ssg.template.gspe + + +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy + +@Retention(RetentionPolicy.SOURCE) +@interface PeekBefore { + ComponentToken.Type[] value() +} \ No newline at end of file diff --git a/template/src/main/groovy/com/jessebrault/ssg/template/gspe/TemplateToScriptConverter.groovy b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/TemplateToScriptConverter.groovy new file mode 100644 index 0000000..452b309 --- /dev/null +++ b/template/src/main/groovy/com/jessebrault/ssg/template/gspe/TemplateToScriptConverter.groovy @@ -0,0 +1,164 @@ +package com.jessebrault.ssg.template.gspe + +import com.jessebrault.fsm.stackfunction.StackFunctionFsmBuilder +import com.jessebrault.fsm.stackfunction.StackFunctionFsmBuilderImpl +import groovy.transform.PackageScope + +import static FsmState.* + +@PackageScope +class TemplateToScriptConverter { + + private static final PatternFunction html = new PatternFunction(~/^(?:[\w\W&&[^<$]]|<(?!%|\p{Lu})|\$(?!\{))+/) + private static final PatternFunction scriptletOpen = new PatternFunction(~/^<%(?!=)/) + private static final PatternFunction expressionScriptletOpen = new PatternFunction(~/^<%=/) + private static final PatternFunction scriptletText = new PatternFunction(~/^.+(?=%>)/) + private static final PatternFunction scriptletClose = new PatternFunction(~/^%>/) + + private static final PatternFunction componentOpen = new PatternFunction(~/^<(?=\p{Lu})/) + private static final PatternFunction componentIdentifier = new PatternFunction(~/^\p{Lu}.*?(?=\s|\\/)/) + private static final PatternFunction attrKeyWithValue = new PatternFunction(~/^\s*[\p{Ll}\p{Lu}0-9_\-]+=/) + private static final PatternFunction attrKeyBoolean = new PatternFunction(~/^\s*[\p{Ll}\p{Lu}0-9_\-]++(?!=)/) + private static final PatternFunction componentSelfClose = new PatternFunction(~/^\s*\/>/) + + private static final PatternFunction attrValueStringOpen = new PatternFunction(~/^["']/) + private static final PatternFunction attrValueStringContents = new PatternFunction(~/^(?:[\w\W&&[^\\"]]|\\\\|\\")*(?=")/) + private static final PatternFunction attrValueStringClose = new PatternFunction(~/["']/) + + private static StackFunctionFsmBuilder getFsmBuilder() { + new StackFunctionFsmBuilderImpl<>() + } + + @SuppressWarnings('GrMethodMayBeStatic') + String convert(String src) { + def b = new StringBuilder() + def stringAcc = new StringBuilder() + + b << 'def getTemplate() {\nreturn { out ->\n' + + def fsm = getFsmBuilder().with { + initialState = HTML + + whileIn(HTML) { + on html exec { + stringAcc << it + } + on scriptletOpen shiftTo SCRIPTLET exec { + if (stringAcc.length() > 0) { + b << 'out << """' << stringAcc.toString() << '""";\n' + stringAcc = new StringBuilder() + } + } + on expressionScriptletOpen shiftTo EXPRESSION_SCRIPTLET exec { + stringAcc << '${' + } + on componentOpen shiftTo COMPONENT_IDENTIFIER exec { + if (stringAcc.length() > 0) { + b << 'out << """' << stringAcc.toString() << '""";\n' + stringAcc = new StringBuilder() + } + } + } + + whileIn(SCRIPTLET) { + on scriptletText exec { + b << it + } + on scriptletClose shiftTo HTML exec { + b << ';\n' + } + } + + whileIn(EXPRESSION_SCRIPTLET) { + on scriptletText exec { + stringAcc << it + } + on scriptletClose shiftTo HTML exec { + stringAcc << '}' + } + } + + whileIn(COMPONENT) { + // tokenize component, figure out body, and tokenize closing component + } + + whileIn(COMPONENT_IDENTIFIER) { + on componentIdentifier shiftTo COMPONENT_ATTR_KEY exec { + b << "out << renderComponent('${ it }') { attr, bodyOut ->\n" + } + onNoMatch() exec { + throw new RuntimeException('expected a Component Identifier') + } + } + + whileIn(COMPONENT_ATTR_KEY) { + on attrKeyWithValue shiftTo COMPONENT_ATTR_VALUE_OPEN exec { String s -> + def trimmed = s.trim() + def key = trimmed.substring(0, trimmed.length() - 1) + b << "attr['${ key }'] = " + } + on attrKeyBoolean exec { String s -> + def trimmed = s.trim() + def key = trimmed.substring(0, trimmed.length() - 1) + b << "attr['${ key }'] = true" + } + on componentSelfClose shiftTo HTML exec { + b << '};\n' + } + onNoMatch() exec { + throw new RuntimeException('expected either an attr key or a closing />') + } + } + + whileIn(COMPONENT_ATTR_VALUE_OPEN) { + on attrValueStringOpen shiftTo COMPONENT_ATTR_VALUE_STRING exec { + b << '"' + } + onNoMatch() exec { + throw new RuntimeException('expected a string opening') + } + } + + whileIn(COMPONENT_ATTR_VALUE_STRING) { + on attrValueStringContents shiftTo COMPONENT_ATTR_VALUE_STRING_CLOSE exec { + b << it + } + onNoMatch() exec { + throw new RuntimeException('expected string contents') + } + } + + whileIn(COMPONENT_ATTR_VALUE_STRING_CLOSE) { + on attrValueStringClose shiftTo COMPONENT_ATTR_KEY exec { + b << '";\n' + } + onNoMatch() exec { + throw new RuntimeException('expected string close') + } + } + + build() + } + + def remaining = src + while (remaining.length() > 0) { + def output = fsm.apply(remaining) + if (output != null) { + remaining = remaining.substring(output.length()) + } else if (output != null && output.length() == 0) { + throw new RuntimeException('output length is zero') + } else { + throw new RuntimeException('output is null') + } + } + + if (fsm.currentState == HTML && stringAcc.length() > 0) { + b << 'out << """' << stringAcc.toString() << '""";\n' + stringAcc = new StringBuilder() + } + b << '}}\n' + + b.toString() + } + +} diff --git a/template/src/test/groovy/com/jessebrault/ssg/template/gspe/ComponentTokenizerTests.groovy b/template/src/test/groovy/com/jessebrault/ssg/template/gspe/ComponentTokenizerTests.groovy new file mode 100644 index 0000000..41d175f --- /dev/null +++ b/template/src/test/groovy/com/jessebrault/ssg/template/gspe/ComponentTokenizerTests.groovy @@ -0,0 +1,107 @@ +package com.jessebrault.ssg.template.gspe + +import org.junit.jupiter.api.Test + +import static com.jessebrault.ssg.template.gspe.ComponentToken.Type.* + +import static org.junit.jupiter.api.Assertions.assertEquals + +class ComponentTokenizerTests { + + private final ComponentTokenizer tokenizer = new ComponentTokenizer() + + @Test + void selfClosingComponent() { + def r = this.tokenizer.tokenize('') + assertEquals(4, r.size()) + assertEquals(LT, r[0].type) + assertEquals(IDENTIFIER, r[1].type) + assertEquals('Test', r[1].text) + assertEquals(FORWARD_SLASH, r[2].type) + assertEquals(GT, r[3].type) + } + + @Test + void selfClosingComponentWithDoubleQuotedString() { + def r = this.tokenizer.tokenize('') + assertEquals(9, r.size()) + + assertEquals(LT, r[0].type) + + assertEquals(IDENTIFIER, r[1].type) + assertEquals('Test', r[1].text) + + assertEquals(KEY, r[2].type) + assertEquals('key', r[2].text) + + assertEquals(EQUALS, r[3].type) + + assertEquals(DOUBLE_QUOTE, r[4].type) + + assertEquals(STRING, r[5].type) + assertEquals('value', r[5].text) + + assertEquals(DOUBLE_QUOTE, r[6].type) + + assertEquals(FORWARD_SLASH, r[7].type) + + assertEquals(GT, r[8].type) + } + + @Test + void selfClosingComponentWithSingleQuotedString() { + def r = this.tokenizer.tokenize("") + assertEquals(9, r.size()) + + assertEquals(LT, r[0].type) + + assertEquals(IDENTIFIER, r[1].type) + assertEquals('Test', r[1].text) + + assertEquals(KEY, r[2].type) + assertEquals('key', r[2].text) + + assertEquals(EQUALS, r[3].type) + + assertEquals(SINGLE_QUOTE, r[4].type) + + assertEquals(STRING, r[5].type) + assertEquals('value', r[5].text) + + assertEquals(SINGLE_QUOTE, r[6].type) + + assertEquals(FORWARD_SLASH, r[7].type) + + assertEquals(GT, r[8].type) + } + + @Test + void componentWithSimpleDollarGroovy() { + def r = this.tokenizer.tokenize('') + assertEquals(10, r.size()) + + assertEquals(LT, r[0].type) + + assertEquals(IDENTIFIER, r[1].type) + assertEquals('Test', r[1].text) + + assertEquals(KEY, r[2].type) + assertEquals('key', r[2].text) + + assertEquals(EQUALS, r[3].type) + + assertEquals(DOLLAR, r[4].type) + + assertEquals(CURLY_OPEN, r[5].type) + + assertEquals(GROOVY, r[6].type) + assertEquals(' test ', r[6].text) + + assertEquals(CURLY_CLOSE, r[7].type) + + assertEquals(FORWARD_SLASH, r[8].type) + + assertEquals(GT, r[9].type) + } + +} 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 new file mode 100644 index 0000000..0759de2 --- /dev/null +++ b/template/src/test/groovy/com/jessebrault/ssg/template/gspe/GspeTemplateEngineTests.groovy @@ -0,0 +1,159 @@ +package com.jessebrault.ssg.template.gspe + + +import groovy.text.TemplateEngine +import org.junit.jupiter.api.Test + +import static org.junit.jupiter.api.Assertions.assertEquals + +class GspeTemplateEngineTests { + + private final TemplateEngine engine = new GspeTemplateEngine(new GspeTemplateEngine.Configuration( + { new GspeTemplate() }, + [], + [] + )) + + @Test + void doctype() { + def src = '' + def r = this.engine.createTemplate(src).make().toString() + assertEquals('', r) + } + + @Test + void handlesNewlines() { + def src = '\n' + def r = this.engine.createTemplate(src).make().toString() + assertEquals(src, r) + } + + @Test + void emptyScriptlet() { + def src = '<%%>' + def r = this.engine.createTemplate(src).make().toString() + assertEquals('', r) + } + + @Test + void simpleOut() { + def src = '<% out << "Hello, World!" %>' + def r = this.engine.createTemplate(src).make().toString() + assertEquals('Hello, World!', r) + } + + @Test + void scriptletInString() { + def src = '">' + def r = this.engine.createTemplate(src).make().toString() + assertEquals('', r) + } + + @Test + void expressionScriptlet() { + def src = '<%= 13 %>' + def r = this.engine.createTemplate(src).make().toString() + assertEquals('13', r) + } + + @Test + void bindingWorks() { + def src = '<%= greeting %>' + def r = this.engine.createTemplate(src).make([greeting: 'Hello, World!']).toString() + assertEquals('Hello, World!', r) + } + + static class CustomBaseTemplate extends GspeTemplate { + + def greeting = 'Greetings!' + def name = 'Jesse' + + @SuppressWarnings('GrMethodMayBeStatic') + def greet() { + 'Hello, World!' + } + + } + + @Test + void baseTemplateMethodsPresent() { + def src = '<%= greet() %>' + def configuration = new GspeTemplateEngine.Configuration({ new CustomBaseTemplate() }, [], []) + def engine = new GspeTemplateEngine(configuration) + def r = engine.createTemplate(src).make().toString() + assertEquals('Hello, World!', r) + } + + @Test + void baseTemplatePropertiesPresent() { + def src = '<%= this.greeting %>' + def configuration = new GspeTemplateEngine.Configuration({ new CustomBaseTemplate() }, [], []) + def engine = new GspeTemplateEngine(configuration) + def r = engine.createTemplate(src).make().toString() + assertEquals('Greetings!', r) + } + + @Test + void bindingOverridesCustomBaseTemplate() { + def src = '<%= greet() %>' + def configuration = new GspeTemplateEngine.Configuration({ new CustomBaseTemplate() }, [], []) + def engine = new GspeTemplateEngine(configuration) + def r = engine.createTemplate(src).make([greet: { "Hello, Person!" }]).toString() + assertEquals('Hello, Person!', r) + } + + static class Greeter implements Component { + + @Override + String render(Map attr, String body) { + "${ attr.greeting }, ${ attr.person }!" + } + + } + + @Test + void selfClosingComponent() { + def src = '' + def configuration = new GspeTemplateEngine.Configuration({ new GspeTemplate() }, [], [new Greeter()]) + def engine = new GspeTemplateEngine(configuration) + def r = engine.createTemplate(src).make().toString() + assertEquals('Hello, World!', r) + } + + @Test + void componentWithGStringAttrValue() { + def src = '' + def configuration = new GspeTemplateEngine.Configuration({ new GspeTemplate() }, [], [new Greeter()]) + def engine = new GspeTemplateEngine(configuration) + def r = engine.createTemplate(src).make().toString() + assertEquals('Hello, person number 13!', r) + } + + @Test + void componentWithGStringAttrValueCanAccessBinding() { + def src = '' + def configuration = new GspeTemplateEngine.Configuration({ new GspeTemplate() }, [], [new Greeter()]) + def engine = new GspeTemplateEngine(configuration) + def r = engine.createTemplate(src).make([name: 'Jesse']).toString() + assertEquals('Hello, person named Jesse!', r) + } + + @Test + void componentWithGStringAttrValueCanAccessBaseTemplateMethod() { + def src = '' + def configuration = new GspeTemplateEngine.Configuration({ new CustomBaseTemplate() }, [], [new Greeter()]) + def engine = new GspeTemplateEngine(configuration) + def r = engine.createTemplate(src).make().toString() + assertEquals('Hello, person named Jesse!', r) + } + + @Test + void componentWithGStringAttrValueCanAccessBaseTemplateProperty() { + def src = '' + def configuration = new GspeTemplateEngine.Configuration({ new CustomBaseTemplate() }, [], [new Greeter()]) + def engine = new GspeTemplateEngine(configuration) + def r = engine.createTemplate(src).make().toString() + assertEquals('Hello, person named Jesse!', r) + } + +} diff --git a/template/src/test/resources/components/Greeting.groovy b/template/src/test/resources/components/Greeting.groovy new file mode 100644 index 0000000..78cdd69 --- /dev/null +++ b/template/src/test/resources/components/Greeting.groovy @@ -0,0 +1,12 @@ +package components + +import com.jessebrault.ssg.template.gspe.Component + +class Greeting implements Component { + + @Override + String render(Map attr, String body) { + "

${ attr.person }, ${ attr.person }!

" + } + +} diff --git a/template/src/test/resources/components/Head.groovy b/template/src/test/resources/components/Head.groovy new file mode 100644 index 0000000..67b6d03 --- /dev/null +++ b/template/src/test/resources/components/Head.groovy @@ -0,0 +1,16 @@ +package components + +import com.jessebrault.ssg.template.gspe.Component + +class Head implements Component { + + @Override + String render(Map attr, String body) { + def b = new StringBuilder() + b << '\n' + b << " ${ attr.title }\n" + b << '\n' + b.toString() + } + +} diff --git a/template/src/test/resources/log4j2.xml b/template/src/test/resources/log4j2.xml new file mode 100644 index 0000000..fe10995 --- /dev/null +++ b/template/src/test/resources/log4j2.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/template/src/test/resources/test.ssg b/template/src/test/resources/test.ssg new file mode 100644 index 0000000..c8b636a --- /dev/null +++ b/template/src/test/resources/test.ssg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file