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 {
|
plugins {
|
||||||
id 'groovy'
|
id 'ssg.common'
|
||||||
id 'application'
|
id 'application'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -10,9 +10,6 @@ repositories {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':lib')
|
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
|
// https://mvnrepository.com/artifact/info.picocli/picocli
|
||||||
implementation 'info.picocli:picocli:4.7.0'
|
implementation 'info.picocli:picocli:4.7.0'
|
||||||
|
|
||||||
@ -27,15 +24,6 @@ dependencies {
|
|||||||
|
|
||||||
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core
|
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core
|
||||||
implementation 'org.apache.logging.log4j:log4j-core:2.19.0'
|
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 {
|
application {
|
||||||
@ -49,10 +37,7 @@ jar {
|
|||||||
|
|
||||||
distributions {
|
distributions {
|
||||||
main {
|
main {
|
||||||
|
//noinspection GroovyAssignabilityCheck
|
||||||
distributionBaseName = 'ssg'
|
distributionBaseName = 'ssg'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
|
||||||
useJUnitPlatform()
|
|
||||||
}
|
|
@ -1,5 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'groovy'
|
id 'ssg.common'
|
||||||
|
id 'ssg.lib'
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
@ -7,9 +8,6 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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
|
// https://mvnrepository.com/artifact/org.apache.groovy/groovy-templates
|
||||||
implementation 'org.apache.groovy:groovy-templates:4.0.7'
|
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
|
// https://mvnrepository.com/artifact/org.commonmark/commonmark-ext-yaml-front-matter
|
||||||
implementation 'org.commonmark:commonmark-ext-yaml-front-matter:0.21.0'
|
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 {
|
jar {
|
||||||
archivesBaseName = 'ssg-lib'
|
archivesBaseName = 'ssg-lib'
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
|
||||||
useJUnitPlatform()
|
|
||||||
}
|
|
@ -1,2 +1,2 @@
|
|||||||
rootProject.name = 'ssg'
|
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