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
|
||||
|
||||
import com.jessebrault.ssg.template.gspe.component.ComponentsContainer
|
||||
import groovy.text.Template
|
||||
|
||||
class GspeTemplate implements Template {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.jessebrault.ssg.template.gspe
|
||||
|
||||
|
||||
import com.jessebrault.ssg.template.gspe.component.Component
|
||||
import com.jessebrault.ssg.template.gspe.component.ComponentsContainer
|
||||
import groovy.text.Template
|
||||
import groovy.text.TemplateEngine
|
||||
import groovy.transform.TupleConstructor
|
||||
|
@ -6,7 +6,6 @@ import groovy.transform.TupleConstructor
|
||||
import java.util.function.Function
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@PackageScope
|
||||
@TupleConstructor(includeFields = true, defaults = false)
|
||||
class PatternFunction implements Function<String, String> {
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
package com.jessebrault.ssg.template.gspe
|
||||
package com.jessebrault.ssg.template.gspe.component
|
||||
|
||||
interface Component {
|
||||
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
|
||||
|
||||
@TupleConstructor(defaults = false)
|
||||
@TupleConstructor
|
||||
class ComponentToken {
|
||||
|
||||
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.FunctionFsmBuilderImpl
|
||||
import com.jessebrault.ssg.template.gspe.PatternFunction
|
||||
|
||||
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.LoggerFactory
|
@ -1,4 +1,4 @@
|
||||
package com.jessebrault.ssg.template.gspe
|
||||
package com.jessebrault.ssg.template.gspe.component
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
import com.jessebrault.ssg.template.gspe.component.Component
|
||||
import groovy.text.TemplateEngine
|
||||
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.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import static com.jessebrault.ssg.template.gspe.ComponentToken.Type.*
|
||||
import static com.jessebrault.ssg.template.gspe.component.ComponentToken.Type.*
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue
|
||||
|
@ -1,6 +1,6 @@
|
||||
package components
|
||||
|
||||
import com.jessebrault.ssg.template.gspe.Component
|
||||
import com.jessebrault.ssg.template.gspe.component.Component
|
||||
|
||||
class Greeting implements Component {
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
package components
|
||||
|
||||
import com.jessebrault.ssg.template.gspe.Component
|
||||
import com.jessebrault.ssg.template.gspe.component.Component
|
||||
|
||||
class Head implements Component {
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user