Working on new Template
This commit is contained in:
parent
2097e280e3
commit
8d37d8883d
@ -1,4 +0,0 @@
|
||||
subprojects {
|
||||
group = 'com.jessebrault.ssg'
|
||||
version = '0.0.1'
|
||||
}
|
26
buildSrc/build.gradle
Normal file
26
buildSrc/build.gradle
Normal file
@ -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'
|
||||
}
|
29
buildSrc/src/main/groovy/ssg.common.gradle
Normal file
29
buildSrc/src/main/groovy/ssg.common.gradle
Normal file
@ -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()
|
||||
}
|
33
buildSrc/src/main/groovy/ssg.lib.gradle
Normal file
33
buildSrc/src/main/groovy/ssg.lib.gradle
Normal file
@ -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'
|
||||
}
|
@ -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()
|
||||
}
|
@ -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()
|
||||
}
|
@ -1,2 +1,2 @@
|
||||
rootProject.name = 'ssg'
|
||||
include 'cli', 'lib'
|
||||
include 'cli', 'lib', 'template'
|
26
template/build.gradle
Normal file
26
template/build.gradle
Normal file
@ -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'
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package com.jessebrault.ssg.template.gspe
|
||||
|
||||
interface Component {
|
||||
String render(Map<String, ?> attr, String body)
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package com.jessebrault.ssg.template.gspe
|
||||
|
||||
interface ComponentFactory {
|
||||
Component get()
|
||||
}
|
@ -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<ComponentToken> tokens
|
||||
private StringBuilder b
|
||||
|
||||
private String identifier
|
||||
|
||||
String parse(List<ComponentToken> tokens) {
|
||||
this.b = new StringBuilder()
|
||||
this.tokens = new LinkedList<>(tokens)
|
||||
|
||||
this.selfClosingComponent()
|
||||
|
||||
this.b.toString()
|
||||
}
|
||||
|
||||
String parse(List<ComponentToken> openingTokens, String bodyClosure, List<ComponentToken> 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<ComponentToken.Type> 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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<Type> types) {
|
||||
types.contains(this)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Type type
|
||||
String text
|
||||
|
||||
}
|
@ -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 greaterThan = 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<String, State, String> getFsmBuilder() {
|
||||
new FunctionFsmBuilderImpl<>()
|
||||
}
|
||||
|
||||
Queue<ComponentToken> tokenize(String src) {
|
||||
Queue<ComponentToken> 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<String>
|
||||
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
|
||||
}
|
||||
|
||||
}
|
@ -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<String, Component> componentCache = [:]
|
||||
private final GroovyClassLoader loader
|
||||
|
||||
ComponentsContainer(Collection<URL> componentDirUrls, Collection<Component> 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<? extends Component>) 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)
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
@ -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<String, String> 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
|
||||
}
|
||||
|
||||
}
|
@ -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<GspeTemplate> ssgTemplateSupplier
|
||||
Collection<URL> componentDirUrls
|
||||
Collection<Component> 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
|
||||
}
|
||||
|
||||
}
|
@ -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<String, String> {
|
||||
|
||||
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 })"
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
@ -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<String, FsmState, String> 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()
|
||||
}
|
||||
|
||||
}
|
@ -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('<Test />')
|
||||
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('<Test key="value" />')
|
||||
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("<Test key='value' />")
|
||||
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('<Test key=${ test } />')
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
@ -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 = '<!DOCTYPE html>'
|
||||
def r = this.engine.createTemplate(src).make().toString()
|
||||
assertEquals('<!DOCTYPE html>', r)
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesNewlines() {
|
||||
def src = '<!DOCTYPE html>\n<html>'
|
||||
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 = '<html lang="<% out << "en" %>">'
|
||||
def r = this.engine.createTemplate(src).make().toString()
|
||||
assertEquals('<html lang="en">', 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<String, ?> attr, String body) {
|
||||
"${ attr.greeting }, ${ attr.person }!"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void selfClosingComponent() {
|
||||
def src = '<Greeter greeting="Hello" person="World" />'
|
||||
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 = '<Greeter greeting="Hello" person="person number ${ 13 }" />'
|
||||
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 = '<Greeter greeting="Hello" person="person named ${ name }" />'
|
||||
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 = '<Greeter greeting="Hello" person="person named ${ getName() }" />'
|
||||
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 = '<Greeter greeting="Hello" person="person named ${ this.name }" />'
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
12
template/src/test/resources/components/Greeting.groovy
Normal file
12
template/src/test/resources/components/Greeting.groovy
Normal file
@ -0,0 +1,12 @@
|
||||
package components
|
||||
|
||||
import com.jessebrault.ssg.template.gspe.Component
|
||||
|
||||
class Greeting implements Component {
|
||||
|
||||
@Override
|
||||
String render(Map<String, ?> attr, String body) {
|
||||
"<h1>${ attr.person }, ${ attr.person }!</h1>"
|
||||
}
|
||||
|
||||
}
|
16
template/src/test/resources/components/Head.groovy
Normal file
16
template/src/test/resources/components/Head.groovy
Normal file
@ -0,0 +1,16 @@
|
||||
package components
|
||||
|
||||
import com.jessebrault.ssg.template.gspe.Component
|
||||
|
||||
class Head implements Component {
|
||||
|
||||
@Override
|
||||
String render(Map<String, ?> attr, String body) {
|
||||
def b = new StringBuilder()
|
||||
b << '<head>\n'
|
||||
b << " <title>${ attr.title }</title>\n"
|
||||
b << '</head>\n'
|
||||
b.toString()
|
||||
}
|
||||
|
||||
}
|
17
template/src/test/resources/log4j2.xml
Normal file
17
template/src/test/resources/log4j2.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Configuration name="ssg" status="WARN">
|
||||
<Appenders>
|
||||
<Console name="standard" target="SYSTEM_OUT">
|
||||
<PatternLayout>
|
||||
<MarkerPatternSelector defaultPattern="%highlight{%-5level} %logger{1} %M %L: %msg%n%ex">
|
||||
<PatternMatch key="FLOW" pattern="%highlight{%-5level} %logger{1} %M %L: %markerSimpleName %msg%n%ex" />
|
||||
</MarkerPatternSelector>
|
||||
</PatternLayout>
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Root level="trace">
|
||||
<AppenderRef ref="standard" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
7
template/src/test/resources/test.ssg
Normal file
7
template/src/test/resources/test.ssg
Normal file
@ -0,0 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<Head title="Greeting Page" />
|
||||
<body>
|
||||
<Greeting person="World" />
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user