component node
This commit is contained in:
parent
5d4df7662a
commit
a4bfea92ef
@ -1,5 +0,0 @@
|
|||||||
package com.jessebrault.ssg.template.gspe
|
|
||||||
|
|
||||||
interface ComponentFactory {
|
|
||||||
Component get()
|
|
||||||
}
|
|
@ -1,256 +0,0 @@
|
|||||||
package com.jessebrault.ssg.template.gspe
|
|
||||||
|
|
||||||
|
|
||||||
import groovy.transform.PackageScope
|
|
||||||
|
|
||||||
import static com.jessebrault.ssg.template.gspe.ComponentToken.Type.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NOT thread safe
|
|
||||||
*/
|
|
||||||
@PackageScope
|
|
||||||
class ComponentParser {
|
|
||||||
|
|
||||||
private Queue<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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,5 +1,6 @@
|
|||||||
package com.jessebrault.ssg.template.gspe
|
package com.jessebrault.ssg.template.gspe
|
||||||
|
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.ComponentsContainer
|
||||||
import groovy.text.Template
|
import groovy.text.Template
|
||||||
|
|
||||||
class GspeTemplate implements Template {
|
class GspeTemplate implements Template {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package com.jessebrault.ssg.template.gspe
|
package com.jessebrault.ssg.template.gspe
|
||||||
|
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.Component
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.ComponentsContainer
|
||||||
import groovy.text.Template
|
import groovy.text.Template
|
||||||
import groovy.text.TemplateEngine
|
import groovy.text.TemplateEngine
|
||||||
import groovy.transform.TupleConstructor
|
import groovy.transform.TupleConstructor
|
||||||
|
@ -6,7 +6,6 @@ import groovy.transform.TupleConstructor
|
|||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
@PackageScope
|
|
||||||
@TupleConstructor(includeFields = true, defaults = false)
|
@TupleConstructor(includeFields = true, defaults = false)
|
||||||
class PatternFunction implements Function<String, String> {
|
class PatternFunction implements Function<String, String> {
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package com.jessebrault.ssg.template.gspe
|
package com.jessebrault.ssg.template.gspe.component
|
||||||
|
|
||||||
interface Component {
|
interface Component {
|
||||||
String render(Map<String, ?> attr, String body)
|
String render(Map<String, ?> attr, String body)
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.jessebrault.ssg.template.gspe.component
|
||||||
|
|
||||||
|
interface ComponentFactory {
|
||||||
|
Component get()
|
||||||
|
}
|
@ -0,0 +1,224 @@
|
|||||||
|
package com.jessebrault.ssg.template.gspe.component
|
||||||
|
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.ComponentToken.Type
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.ComponentNode
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.DollarReferenceValue
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.DollarScriptletValue
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.ExpressionScriptletValue
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.GStringValue
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.KeyAndValue
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.KeysAndValues
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.Node
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.ScriptletValue
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.StringValue
|
||||||
|
import groovy.transform.PackageScope
|
||||||
|
|
||||||
|
import static com.jessebrault.ssg.template.gspe.component.ComponentToken.Type.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOT thread safe
|
||||||
|
*/
|
||||||
|
@PackageScope
|
||||||
|
class ComponentParser {
|
||||||
|
|
||||||
|
private Queue<ComponentToken> tokens
|
||||||
|
private String currentIdentifier
|
||||||
|
|
||||||
|
ComponentNode parse(Queue<ComponentToken> tokens) {
|
||||||
|
this.tokens = tokens
|
||||||
|
this.selfClosingComponent()
|
||||||
|
}
|
||||||
|
|
||||||
|
String parse(Queue<ComponentToken> openingTokens, String bodyClosure, Queue<ComponentToken> closingTokens) {
|
||||||
|
this.tokens = openingTokens
|
||||||
|
def componentNode = this.openingComponent()
|
||||||
|
|
||||||
|
componentNode.body = bodyClosure
|
||||||
|
|
||||||
|
this.tokens = closingTokens
|
||||||
|
this.closingComponent()
|
||||||
|
|
||||||
|
componentNode
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void error(Collection<Type> expectedTypes, ComponentToken actual) {
|
||||||
|
throw new RuntimeException("expected ${ expectedTypes.join(' or ') } but got ${ actual ? "'${ actual }'" : 'null' }")
|
||||||
|
}
|
||||||
|
|
||||||
|
private ComponentToken expect(Collection<Type> types) {
|
||||||
|
def t = this.tokens.poll()
|
||||||
|
if (!t || !t.type.isAnyOf(types)) {
|
||||||
|
error(types, t)
|
||||||
|
}
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
private ComponentToken expect(Type type) {
|
||||||
|
this.expect([type])
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean peek(Type type) {
|
||||||
|
def t = this.tokens.peek()
|
||||||
|
t && t.type == type
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean peekSecond(Type type) {
|
||||||
|
def t = this.tokens[1]
|
||||||
|
t && t.type == type
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean peekThird(Type type) {
|
||||||
|
def t = this.tokens[2]
|
||||||
|
t && t.type == type
|
||||||
|
}
|
||||||
|
|
||||||
|
private ComponentNode selfClosingComponent() {
|
||||||
|
this.startOfOpeningOrSelfClosingComponent()
|
||||||
|
def keysAndValues = this.keysAndValues()
|
||||||
|
this.expect(FORWARD_SLASH)
|
||||||
|
this.expect(GT)
|
||||||
|
new ComponentNode().tap {
|
||||||
|
it.identifier = this.currentIdentifier
|
||||||
|
it.children << keysAndValues
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ComponentNode openingComponent() {
|
||||||
|
this.startOfOpeningOrSelfClosingComponent()
|
||||||
|
def keysAndValues = this.keysAndValues()
|
||||||
|
this.expect(GT)
|
||||||
|
new ComponentNode().tap {
|
||||||
|
it.identifier = this.currentIdentifier
|
||||||
|
it.children << keysAndValues
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closingComponent() {
|
||||||
|
this.expect(LT)
|
||||||
|
this.expect(FORWARD_SLASH)
|
||||||
|
def identifierToken = this.expect(IDENTIFIER)
|
||||||
|
if (identifierToken.text != this.currentIdentifier) {
|
||||||
|
throw new RuntimeException("expected '${ this.currentIdentifier }' but got '${ t2.text }'")
|
||||||
|
}
|
||||||
|
this.expect(GT)
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startOfOpeningOrSelfClosingComponent() {
|
||||||
|
this.expect(LT)
|
||||||
|
def identifierToken = this.expect(IDENTIFIER)
|
||||||
|
this.currentIdentifier = identifierToken.text
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeysAndValues keysAndValues() {
|
||||||
|
List<Node> children = []
|
||||||
|
while (true) {
|
||||||
|
if (this.peek(KEY)) {
|
||||||
|
def keyAndValue = this.keyAndValue()
|
||||||
|
children << keyAndValue
|
||||||
|
} else if (this.peek(FORWARD_SLASH)) {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
error([KEY, FORWARD_SLASH], this.tokens.poll())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
new KeysAndValues().tap {
|
||||||
|
it.children.addAll(children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PeekBefore(KEY)
|
||||||
|
private KeyAndValue keyAndValue() {
|
||||||
|
def keyToken = this.expect(KEY)
|
||||||
|
this.expect(EQUALS)
|
||||||
|
def value = this.value()
|
||||||
|
new KeyAndValue().tap {
|
||||||
|
key = keyToken.text
|
||||||
|
it.children << value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node value() {
|
||||||
|
if (this.peek(DOUBLE_QUOTE)) {
|
||||||
|
return this.doubleQuoteStringValue()
|
||||||
|
} else if (this.peek(SINGLE_QUOTE)) {
|
||||||
|
return this.singleQuoteStringValue()
|
||||||
|
} else if (this.peek(DOLLAR) && this.peekSecond(GROOVY_IDENTIFIER)) {
|
||||||
|
return this.dollarReferenceValue()
|
||||||
|
} else if (this.peek(DOLLAR) && this.peekSecond(CURLY_OPEN)) {
|
||||||
|
return this.dollarScriptletValue()
|
||||||
|
} else if (this.peek(LT) && this.peekSecond(PERCENT) && this.peekThird(EQUALS)) {
|
||||||
|
return this.expressionScriptletValue()
|
||||||
|
} else if (this.peek(LT) && this.peekSecond(PERCENT)) {
|
||||||
|
return this.scriptletValue()
|
||||||
|
} else {
|
||||||
|
error([DOUBLE_QUOTE, SINGLE_QUOTE, DOLLAR, LT], this.tokens.poll())
|
||||||
|
}
|
||||||
|
throw new RuntimeException('should not get here')
|
||||||
|
}
|
||||||
|
|
||||||
|
@PeekBefore(DOUBLE_QUOTE)
|
||||||
|
private GStringValue doubleQuoteStringValue() {
|
||||||
|
this.expect(DOUBLE_QUOTE)
|
||||||
|
def stringToken = this.expect(STRING)
|
||||||
|
this.expect(DOUBLE_QUOTE)
|
||||||
|
new GStringValue().tap {
|
||||||
|
gString = stringToken.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PeekBefore(SINGLE_QUOTE)
|
||||||
|
private StringValue singleQuoteStringValue() {
|
||||||
|
this.expect(SINGLE_QUOTE)
|
||||||
|
def stringToken = this.expect(STRING)
|
||||||
|
this.expect(SINGLE_QUOTE)
|
||||||
|
new StringValue().tap {
|
||||||
|
string = stringToken.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PeekBefore([DOLLAR, GROOVY_IDENTIFIER])
|
||||||
|
private DollarReferenceValue dollarReferenceValue() {
|
||||||
|
this.expect(DOLLAR)
|
||||||
|
def groovyIdentifierToken = this.expect(GROOVY_IDENTIFIER)
|
||||||
|
new DollarReferenceValue().tap {
|
||||||
|
reference = groovyIdentifierToken.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PeekBefore([DOLLAR, CURLY_OPEN])
|
||||||
|
private DollarScriptletValue dollarScriptletValue() {
|
||||||
|
this.expect(DOLLAR)
|
||||||
|
this.expect(CURLY_OPEN)
|
||||||
|
def groovyToken = this.expect(GROOVY)
|
||||||
|
this.expect(CURLY_CLOSE)
|
||||||
|
new DollarScriptletValue().tap {
|
||||||
|
scriptlet = groovyToken.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PeekBefore([LT, PERCENT, EQUALS])
|
||||||
|
private ExpressionScriptletValue expressionScriptletValue() {
|
||||||
|
this.expect(LT)
|
||||||
|
this.expect(PERCENT)
|
||||||
|
this.expect(EQUALS)
|
||||||
|
def groovyToken = this.expect(GROOVY)
|
||||||
|
this.expect(PERCENT)
|
||||||
|
this.expect(GT)
|
||||||
|
new ExpressionScriptletValue().tap {
|
||||||
|
scriptlet = groovyToken.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PeekBefore([LT, PERCENT])
|
||||||
|
private ScriptletValue scriptletValue() {
|
||||||
|
this.expect(LT)
|
||||||
|
this.expect(PERCENT)
|
||||||
|
def groovyToken = this.expect(GROOVY)
|
||||||
|
this.expect(PERCENT)
|
||||||
|
this.expect(GT)
|
||||||
|
new ScriptletValue().tap {
|
||||||
|
scriptlet = groovyToken.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
package com.jessebrault.ssg.template.gspe.component
|
||||||
|
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.BooleanValue
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.ComponentNode
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.DollarReferenceValue
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.DollarScriptletValue
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.ExpressionScriptletValue
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.GStringValue
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.KeyAndValue
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.KeysAndValues
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.NodeVisitor
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.ScriptletValue
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.StringValue
|
||||||
|
|
||||||
|
// NOT THREAD SAFE, and must be used exactly once
|
||||||
|
class ComponentToScriptVisitor extends NodeVisitor {
|
||||||
|
|
||||||
|
private final Writer w = new StringWriter()
|
||||||
|
private final IndentPrinter p = new IndentPrinter(this.w, ' ', true, true)
|
||||||
|
|
||||||
|
String getResult() {
|
||||||
|
w.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
void visit(ComponentNode componentNode) {
|
||||||
|
p.println('{ attr, bodyOut ->')
|
||||||
|
p.incrementIndent()
|
||||||
|
super.visit(componentNode)
|
||||||
|
if (componentNode.body != null) {
|
||||||
|
p.println("bodyOut << ${ componentNode.body };")
|
||||||
|
}
|
||||||
|
p.decrementIndent()
|
||||||
|
p.println('};')
|
||||||
|
}
|
||||||
|
|
||||||
|
void visit(KeysAndValues keysAndValues) {
|
||||||
|
p.println('attr {')
|
||||||
|
p.incrementIndent()
|
||||||
|
super.visit(keysAndValues)
|
||||||
|
p.decrementIndent()
|
||||||
|
p.println('};')
|
||||||
|
}
|
||||||
|
|
||||||
|
void visit(KeyAndValue keyAndValue) {
|
||||||
|
p.printIndent()
|
||||||
|
p.print("${ keyAndValue.key } = ")
|
||||||
|
super.visit(keyAndValue)
|
||||||
|
p.print(';\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
void visit(GStringValue gStringValue) {
|
||||||
|
p.print("\"${ gStringValue.gString }\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
void visit(StringValue stringValue) {
|
||||||
|
p.print("'${ stringValue.string }'")
|
||||||
|
}
|
||||||
|
|
||||||
|
void visit(DollarReferenceValue dollarReferenceValue) {
|
||||||
|
p.print(dollarReferenceValue.reference)
|
||||||
|
}
|
||||||
|
|
||||||
|
void visit(DollarScriptletValue dollarScriptletValue) {
|
||||||
|
p.print(dollarScriptletValue.scriptlet)
|
||||||
|
}
|
||||||
|
|
||||||
|
void visit(ScriptletValue scriptletValue) {
|
||||||
|
p.println("render { out ->")
|
||||||
|
p.incrementIndent()
|
||||||
|
p.print(scriptletValue.scriptlet)
|
||||||
|
p.decrementIndent()
|
||||||
|
p.println('}')
|
||||||
|
}
|
||||||
|
|
||||||
|
void visit(ExpressionScriptletValue expressionScriptletValue) {
|
||||||
|
p.print(expressionScriptletValue.scriptlet)
|
||||||
|
}
|
||||||
|
|
||||||
|
void visit(BooleanValue booleanValue) {
|
||||||
|
p.print(booleanValue.value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
package com.jessebrault.ssg.template.gspe
|
package com.jessebrault.ssg.template.gspe.component
|
||||||
|
|
||||||
import groovy.transform.TupleConstructor
|
import groovy.transform.TupleConstructor
|
||||||
|
|
||||||
@TupleConstructor(defaults = false)
|
@TupleConstructor
|
||||||
class ComponentToken {
|
class ComponentToken {
|
||||||
|
|
||||||
enum Type {
|
enum Type {
|
@ -1,7 +1,8 @@
|
|||||||
package com.jessebrault.ssg.template.gspe
|
package com.jessebrault.ssg.template.gspe.component
|
||||||
|
|
||||||
import com.jessebrault.fsm.function.FunctionFsmBuilder
|
import com.jessebrault.fsm.function.FunctionFsmBuilder
|
||||||
import com.jessebrault.fsm.function.FunctionFsmBuilderImpl
|
import com.jessebrault.fsm.function.FunctionFsmBuilderImpl
|
||||||
|
import com.jessebrault.ssg.template.gspe.PatternFunction
|
||||||
|
|
||||||
import static ComponentToken.Type
|
import static ComponentToken.Type
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package com.jessebrault.ssg.template.gspe
|
package com.jessebrault.ssg.template.gspe.component
|
||||||
|
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
@ -1,4 +1,4 @@
|
|||||||
package com.jessebrault.ssg.template.gspe
|
package com.jessebrault.ssg.template.gspe.component
|
||||||
|
|
||||||
|
|
||||||
import java.lang.annotation.Retention
|
import java.lang.annotation.Retention
|
@ -0,0 +1,11 @@
|
|||||||
|
package com.jessebrault.ssg.template.gspe.component.node
|
||||||
|
|
||||||
|
class BooleanValue extends Node {
|
||||||
|
boolean value
|
||||||
|
|
||||||
|
@Override
|
||||||
|
String toString() {
|
||||||
|
"BooleanValue(${ this.value })"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package com.jessebrault.ssg.template.gspe.component.node
|
||||||
|
|
||||||
|
class ComponentNode extends Node {
|
||||||
|
|
||||||
|
String identifier
|
||||||
|
String body
|
||||||
|
|
||||||
|
@Override
|
||||||
|
String toString() {
|
||||||
|
"ComponentNode(identifier: ${ this.identifier }, body: ${ this.body }, children: ${ this.children })"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.jessebrault.ssg.template.gspe.component.node
|
||||||
|
|
||||||
|
class DollarReferenceValue extends Node {
|
||||||
|
|
||||||
|
String reference
|
||||||
|
|
||||||
|
@Override
|
||||||
|
String toString() {
|
||||||
|
"DollarReferenceValue(${ this.reference })"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.jessebrault.ssg.template.gspe.component.node
|
||||||
|
|
||||||
|
class DollarScriptletValue extends Node {
|
||||||
|
|
||||||
|
String scriptlet
|
||||||
|
|
||||||
|
@Override
|
||||||
|
String toString() {
|
||||||
|
"DollarScriptletValue(${ this.scriptlet })"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.jessebrault.ssg.template.gspe.component.node
|
||||||
|
|
||||||
|
class ExpressionScriptletValue extends Node {
|
||||||
|
|
||||||
|
String scriptlet
|
||||||
|
|
||||||
|
@Override
|
||||||
|
String toString() {
|
||||||
|
"ExpressionScriptletValue(${ this.scriptlet })"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.jessebrault.ssg.template.gspe.component.node
|
||||||
|
|
||||||
|
class GStringValue extends Node {
|
||||||
|
|
||||||
|
String gString
|
||||||
|
|
||||||
|
@Override
|
||||||
|
String toString() {
|
||||||
|
"GStringValue(${ this.gString })"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.jessebrault.ssg.template.gspe.component.node
|
||||||
|
|
||||||
|
class KeyAndValue extends Node {
|
||||||
|
|
||||||
|
String key
|
||||||
|
|
||||||
|
@Override
|
||||||
|
String toString() {
|
||||||
|
"KeyAndValue(key: ${ this.key }, children: ${ this.children })"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package com.jessebrault.ssg.template.gspe.component.node
|
||||||
|
|
||||||
|
class KeysAndValues extends Node {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
String toString() {
|
||||||
|
"KeysAndValues(${ this.children })"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.jessebrault.ssg.template.gspe.component.node
|
||||||
|
|
||||||
|
abstract class Node {
|
||||||
|
List<Node> children = []
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package com.jessebrault.ssg.template.gspe.component.node
|
||||||
|
|
||||||
|
abstract class NodeVisitor {
|
||||||
|
|
||||||
|
void visit(Node node) {
|
||||||
|
this.visitChildren(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
void visitChildren(Node node) {
|
||||||
|
node.children.each {
|
||||||
|
this.visit(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.jessebrault.ssg.template.gspe.component.node
|
||||||
|
|
||||||
|
class ScriptletValue extends Node {
|
||||||
|
|
||||||
|
String scriptlet
|
||||||
|
|
||||||
|
@Override
|
||||||
|
String toString() {
|
||||||
|
"ScriptletValue(${ this.scriptlet })"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.jessebrault.ssg.template.gspe.component.node
|
||||||
|
|
||||||
|
class StringValue extends Node {
|
||||||
|
|
||||||
|
String string
|
||||||
|
|
||||||
|
@Override
|
||||||
|
String toString() {
|
||||||
|
"StringValue(${ this.string })"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
package com.jessebrault.ssg.template.gspe
|
package com.jessebrault.ssg.template.gspe
|
||||||
|
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.Component
|
||||||
import groovy.text.TemplateEngine
|
import groovy.text.TemplateEngine
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
@ -0,0 +1,140 @@
|
|||||||
|
package com.jessebrault.ssg.template.gspe.component
|
||||||
|
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.ComponentNode
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.GStringValue
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.KeyAndValue
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.KeysAndValues
|
||||||
|
import com.jessebrault.ssg.template.gspe.component.node.Node
|
||||||
|
import groovy.transform.stc.ClosureParams
|
||||||
|
import groovy.transform.stc.FirstParam
|
||||||
|
import groovy.transform.stc.SimpleType
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
import static com.jessebrault.ssg.template.gspe.component.ComponentToken.Type.*
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
|
||||||
|
class ComponentParserTests {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ComponentParserTests)
|
||||||
|
|
||||||
|
private static class NodeSpec<T extends Node> {
|
||||||
|
|
||||||
|
Class<T> nodeClass
|
||||||
|
Closure<Void> tests
|
||||||
|
|
||||||
|
NodeSpec(
|
||||||
|
Class<T> nodeClass,
|
||||||
|
@DelegatesTo(value = NodeTester, strategy = Closure.DELEGATE_FIRST)
|
||||||
|
@ClosureParams(FirstParam.FirstGenericType)
|
||||||
|
Closure<Void> tests = null
|
||||||
|
) {
|
||||||
|
this.nodeClass = Objects.requireNonNull(nodeClass)
|
||||||
|
this.tests = tests
|
||||||
|
}
|
||||||
|
|
||||||
|
void test(Node actual) {
|
||||||
|
logger.debug('actual: {}', actual)
|
||||||
|
assertTrue(nodeClass.isAssignableFrom(actual.class))
|
||||||
|
if (this.tests != null) {
|
||||||
|
def nodeTester = new NodeTester()
|
||||||
|
this.tests.setDelegate(nodeTester)
|
||||||
|
this.tests.setResolveStrategy(Closure.DELEGATE_FIRST)
|
||||||
|
this.tests(actual)
|
||||||
|
|
||||||
|
def childIterator = actual.children.iterator()
|
||||||
|
assertEquals(nodeTester.childSpecs.size(), actual.children.size())
|
||||||
|
|
||||||
|
nodeTester.childSpecs.each {
|
||||||
|
assertTrue(childIterator.hasNext())
|
||||||
|
def next = childIterator.next()
|
||||||
|
it.test(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
String toString() {
|
||||||
|
"NodeSpec(${ this.nodeClass.simpleName })"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NodeTester {
|
||||||
|
|
||||||
|
List<NodeSpec<? extends Node>> childSpecs = []
|
||||||
|
|
||||||
|
def <T extends Node> void expect(
|
||||||
|
Class<T> childNodeClass,
|
||||||
|
@DelegatesTo(value = NodeTester, strategy = Closure.DELEGATE_ONLY)
|
||||||
|
@ClosureParams(FirstParam.FirstGenericType)
|
||||||
|
Closure<Void> furtherTests
|
||||||
|
) {
|
||||||
|
this.childSpecs << new NodeSpec<T>(childNodeClass, furtherTests)
|
||||||
|
}
|
||||||
|
|
||||||
|
void expect(Class nodeClass) {
|
||||||
|
this.childSpecs << new NodeSpec(nodeClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ComponentParser parser = new ComponentParser()
|
||||||
|
|
||||||
|
private void selfClosing(
|
||||||
|
List<ComponentToken> tokens,
|
||||||
|
@DelegatesTo(value = NodeTester, strategy = Closure.DELEGATE_FIRST)
|
||||||
|
@ClosureParams(value = SimpleType, options = ['com.jessebrault.ssg.template.gspe.component.node.ComponentNode'])
|
||||||
|
Closure<Void> tests
|
||||||
|
) {
|
||||||
|
def componentNode = this.parser.parse(tokens)
|
||||||
|
logger.debug('componentNode: {}', componentNode)
|
||||||
|
|
||||||
|
def componentSpec = new NodeSpec(ComponentNode, tests)
|
||||||
|
logger.debug('nodeSpec: {}', componentSpec)
|
||||||
|
componentSpec.test(componentNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selfClosingNoKeysOrValues() {
|
||||||
|
this.selfClosing([
|
||||||
|
new ComponentToken(LT),
|
||||||
|
new ComponentToken(IDENTIFIER, 'Test'),
|
||||||
|
new ComponentToken(FORWARD_SLASH),
|
||||||
|
new ComponentToken(GT)
|
||||||
|
]) {
|
||||||
|
assertEquals('Test', it.identifier)
|
||||||
|
expect(KeysAndValues) {
|
||||||
|
assertEquals(0, it.children.size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selfClosingWithGStringValue() {
|
||||||
|
this.selfClosing([
|
||||||
|
new ComponentToken(LT),
|
||||||
|
new ComponentToken(IDENTIFIER, 'Test'),
|
||||||
|
new ComponentToken(KEY, 'test'),
|
||||||
|
new ComponentToken(EQUALS),
|
||||||
|
new ComponentToken(DOUBLE_QUOTE),
|
||||||
|
new ComponentToken(STRING, 'Hello, World!'),
|
||||||
|
new ComponentToken(DOUBLE_QUOTE),
|
||||||
|
new ComponentToken(FORWARD_SLASH),
|
||||||
|
new ComponentToken(GT)
|
||||||
|
]) {
|
||||||
|
assertEquals('Test', it.identifier)
|
||||||
|
expect(KeysAndValues) {
|
||||||
|
expect(KeyAndValue) {
|
||||||
|
assertEquals('test', it.key)
|
||||||
|
expect(GStringValue) {
|
||||||
|
assertEquals('Hello, World!', it.gString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,10 +1,11 @@
|
|||||||
package com.jessebrault.ssg.template.gspe
|
package com.jessebrault.ssg.template.gspe.component
|
||||||
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
import static com.jessebrault.ssg.template.gspe.ComponentToken.Type.*
|
import static com.jessebrault.ssg.template.gspe.component.ComponentToken.Type.*
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals
|
import static org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue
|
import static org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
package components
|
package components
|
||||||
|
|
||||||
import com.jessebrault.ssg.template.gspe.Component
|
import com.jessebrault.ssg.template.gspe.component.Component
|
||||||
|
|
||||||
class Greeting implements Component {
|
class Greeting implements Component {
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package components
|
package components
|
||||||
|
|
||||||
import com.jessebrault.ssg.template.gspe.Component
|
import com.jessebrault.ssg.template.gspe.component.Component
|
||||||
|
|
||||||
class Head implements Component {
|
class Head implements Component {
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user