Clean up.

This commit is contained in:
Jesse Brault 2023-02-11 09:53:20 +01:00
parent 610eac927b
commit efecc24204
72 changed files with 1 additions and 3147 deletions

View File

@ -1,25 +0,0 @@
plugins {
id 'ssg.common'
id 'ssg.lib'
}
repositories {
mavenCentral()
}
dependencies {
// 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'
testRuntimeOnly project(':gcp-impl')
}
jar {
archivesBaseName = 'gcp-api'
}

View File

@ -1,5 +0,0 @@
package com.jessebrault.gcp
interface Component {
String render(Map<String, ?> attr, String body)
}

View File

@ -1,5 +0,0 @@
package com.jessebrault.gcp
interface ComponentFactory {
Component get()
}

View File

@ -1,43 +0,0 @@
package com.jessebrault.gcp
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)
}
}

View File

@ -1,31 +0,0 @@
package com.jessebrault.gcp
import groovy.text.Template
class GcpTemplate 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
}
}

View File

@ -1,61 +0,0 @@
package com.jessebrault.gcp
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 GcpTemplateEngine extends TemplateEngine {
private static final Logger logger = LoggerFactory.getLogger(GcpTemplateEngine)
private static GcpToScriptConverter getConverter() {
ServiceLoader.load(GcpToScriptConverter).findFirst().orElseThrow({
new NullPointerException('Could not find an implementation of GcpToScriptConverter')
})
}
@TupleConstructor(defaults = false)
static class Configuration {
Supplier<GcpTemplate> 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
GcpTemplateEngine(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 = getConverter()
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
}
}

View File

@ -1,5 +0,0 @@
package com.jessebrault.gcp
interface GcpToScriptConverter {
String convert(String gcpSrc)
}

View File

@ -1,49 +0,0 @@
package com.jessebrault.gcp.tokenizer;
import java.util.Collection;
public interface Token {
enum Type {
TEXT,
DOLLAR,
GROOVY_REFERENCE,
CURLY_OPEN,
SCRIPTLET,
CURLY_CLOSE,
BLOCK_SCRIPTLET_OPEN,
EXPRESSION_SCRIPTLET_OPEN,
SCRIPTLET_CLOSE,
CLASS_NAME,
PACKAGE_NAME,
DOT,
WHITESPACE,
KEY,
EQUALS,
DOUBLE_QUOTE,
STRING,
SINGLE_QUOTE,
COMPONENT_START,
FORWARD_SLASH,
COMPONENT_END,
;
boolean isAnyOf(Collection<Type> types) {
return types.contains(this);
}
}
Type getType();
CharSequence getText();
int getInputIndex();
int getLine();
int getCol();
}

View File

@ -1,28 +0,0 @@
package com.jessebrault.gcp.tokenizer;
import java.util.LinkedList;
import java.util.Queue;
public interface Tokenizer {
enum State {
TEXT,
COMPONENT_NAME,
COMPONENT_KEYS_AND_VALUES
}
void start(CharSequence input, int startOffset, int endOffset, State initialState);
boolean hasNext();
Token next();
State getCurrentState();
default Queue<Token> tokenizeAll(CharSequence input, State initialState) {
this.start(input, 0, input.length(), initialState);
final Queue<Token> tokens = new LinkedList<>();
while (this.hasNext()) {
tokens.add(this.next());
}
return tokens;
}
}

View File

@ -1,159 +0,0 @@
package com.jessebrault.gcp
import groovy.text.TemplateEngine
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
class GcpTemplateEngineIntegrationTests {
private final TemplateEngine engine = new GcpTemplateEngine(new GcpTemplateEngine.Configuration(
{ new GcpTemplate() },
[],
[]
))
@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 GcpTemplate {
def greeting = 'Greetings!'
def name = 'Jesse'
@SuppressWarnings('GrMethodMayBeStatic')
def greet() {
'Hello, World!'
}
}
@Test
void baseTemplateMethodsPresent() {
def src = '<%= greet() %>'
def configuration = new GcpTemplateEngine.Configuration({ new CustomBaseTemplate() }, [], [])
def engine = new GcpTemplateEngine(configuration)
def r = engine.createTemplate(src).make().toString()
assertEquals('Hello, World!', r)
}
@Test
void baseTemplatePropertiesPresent() {
def src = '<%= this.greeting %>'
def configuration = new GcpTemplateEngine.Configuration({ new CustomBaseTemplate() }, [], [])
def engine = new GcpTemplateEngine(configuration)
def r = engine.createTemplate(src).make().toString()
assertEquals('Greetings!', r)
}
@Test
void bindingOverridesCustomBaseTemplate() {
def src = '<%= greet() %>'
def configuration = new GcpTemplateEngine.Configuration({ new CustomBaseTemplate() }, [], [])
def engine = new GcpTemplateEngine(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 GcpTemplateEngine.Configuration({ new GcpTemplate() }, [], [new Greeter()])
def engine = new GcpTemplateEngine(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 GcpTemplateEngine.Configuration({ new GcpTemplate() }, [], [new Greeter()])
def engine = new GcpTemplateEngine(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 GcpTemplateEngine.Configuration({ new GcpTemplate() }, [], [new Greeter()])
def engine = new GcpTemplateEngine(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 GcpTemplateEngine.Configuration({ new CustomBaseTemplate() }, [], [new Greeter()])
def engine = new GcpTemplateEngine(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 GcpTemplateEngine.Configuration({ new CustomBaseTemplate() }, [], [new Greeter()])
def engine = new GcpTemplateEngine(configuration)
def r = engine.createTemplate(src).make().toString()
assertEquals('Hello, person named Jesse!', r)
}
}

View File

@ -1,12 +0,0 @@
package components
import com.jessebrault.gcp.Component
class Greeting implements Component {
@Override
String render(Map<String, ?> attr, String body) {
"<h1>${ attr.person }, ${ attr.person }!</h1>"
}
}

View File

@ -1,16 +0,0 @@
package components
import com.jessebrault.gcp.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()
}
}

View File

@ -1,17 +0,0 @@
<?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>

View File

@ -1,9 +0,0 @@
<!DOCTYPE html>
<html>
<Head val=${ frontMatter.test } val=$test val="Hello!" val='Hello!'>
<meta val=${ binding.someVar } />
</Head>
<body>
</body>
</html>

View File

@ -1,7 +0,0 @@
<!DOCTYPE html>
<html>
<Head title="Greeting Page" />
<body>
<Greeting person="World" />
</body>
</html>

View File

@ -1,25 +0,0 @@
plugins {
id 'ssg.common'
id 'ssg.lib'
}
repositories {
mavenCentral()
}
dependencies {
implementation project(':gcp-api')
// 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 = 'gcp-impl'
}

View File

@ -1,80 +0,0 @@
package com.jessebrault.gcp
import com.jessebrault.gcp.groovy.BlockScriptletParser
import com.jessebrault.gcp.groovy.DollarReferenceParser
import com.jessebrault.gcp.groovy.DollarScriptletParser
import com.jessebrault.gcp.groovy.ExpressionScriptletParser
import com.jessebrault.gcp.node.Document
import com.jessebrault.gcp.node.DollarReference
import com.jessebrault.gcp.node.DollarScriptlet
import com.jessebrault.gcp.node.ExpressionScriptlet
import com.jessebrault.gcp.node.Html
import com.jessebrault.gcp.node.BlockScriptlet
import java.util.regex.Matcher
import java.util.regex.Pattern
class GcpParser {
// private static enum State {
// HTML, DOLLAR_GROOVY, SCRIPTLET, EXPRESSION_SCRIPTLET
// }
//
// private static FunctionFsmBuilder<String, State, String> getFsmBuilder() {
// new FunctionFsmBuilderImpl<>()
// }
private static final Pattern html = ~/^(?:[\w\W&&[^<$]]|<(?![%\p{Lu}]))+/
private static final Pattern groovyIdentifier = ~/^[a-zA-Z_\u0024\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\ufff3][a-zA-Z_\u00240-9\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\ufff3]*/
static Document parse(String gcp) {
}
private static Document document(String gcp) {
def document = new Document()
def remaining = gcp
while (remaining.length() > 0) {
Matcher m
DollarReferenceParser.Result dollarReferenceResult
DollarScriptletParser.Result dollarScriptletResult
BlockScriptletParser.Result blockScriptletResult
ExpressionScriptletParser.Result expressionScriptletResult
String match
if ((m = html.matcher(remaining)).find()) {
match = m.group()
document.children << new Html().tap {
text = match
}
} else if (dollarReferenceResult = DollarReferenceParser.parse(remaining)) {
match = dollarReferenceResult.fullMatch
document.children << new DollarReference().tap {
reference = dollarReferenceResult.reference
}
} else if (dollarScriptletResult = DollarScriptletParser.parseResult(remaining)) {
match = dollarScriptletResult.fullMatch
document.children << new DollarScriptlet().tap {
scriptlet = dollarScriptletResult.scriptlet
}
} else if (blockScriptletResult = BlockScriptletParser.parseResult(remaining)) {
match = blockScriptletResult.fullMatch
document.children << new BlockScriptlet().tap {
scriptlet = blockScriptletResult.scriptlet
}
} else if (expressionScriptletResult = ExpressionScriptletParser.parseResult(remaining)) {
match = expressionScriptletResult.fullMatch
document.children << new ExpressionScriptlet().tap {
scriptlet = expressionScriptletResult.scriptlet
}
}
remaining = remaining.substring(match.length())
}
document
}
}

View File

@ -1,180 +0,0 @@
package com.jessebrault.gcp
import com.jessebrault.fsm.stackfunction.StackFunctionFsmBuilder
import com.jessebrault.fsm.stackfunction.StackFunctionFsmBuilderImpl
import com.jessebrault.gcp.util.PatternFunction
class GcpToScriptConverterImpl implements GcpToScriptConverter {
enum State {
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
}
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, State, String> getFsmBuilder() {
new StackFunctionFsmBuilderImpl<>()
}
@Override
String convert(String src) {
def b = new StringBuilder()
def stringAcc = new StringBuilder()
b << 'def getTemplate() {\nreturn { out ->\n'
def fsm = getFsmBuilder().with {
initialState = State.HTML
whileIn(State.HTML) {
on html exec {
stringAcc << it
}
on scriptletOpen shiftTo State.SCRIPTLET exec {
if (stringAcc.length() > 0) {
b << 'out << """' << stringAcc.toString() << '""";\n'
stringAcc = new StringBuilder()
}
}
on expressionScriptletOpen shiftTo State.EXPRESSION_SCRIPTLET exec {
stringAcc << '${'
}
on componentOpen shiftTo State.COMPONENT_IDENTIFIER exec {
if (stringAcc.length() > 0) {
b << 'out << """' << stringAcc.toString() << '""";\n'
stringAcc = new StringBuilder()
}
}
}
whileIn(State.SCRIPTLET) {
on scriptletText exec {
b << it
}
on scriptletClose shiftTo HTML exec {
b << ';\n'
}
}
whileIn(State.EXPRESSION_SCRIPTLET) {
on scriptletText exec {
stringAcc << it
}
on scriptletClose shiftTo HTML exec {
stringAcc << '}'
}
}
whileIn(State.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()
}
}

View File

@ -1,221 +0,0 @@
package com.jessebrault.gcp.component
import com.jessebrault.gcp.component.ComponentToken.Type
import com.jessebrault.gcp.component.node.ComponentRoot
import com.jessebrault.gcp.component.node.DollarReferenceValue
import com.jessebrault.gcp.component.node.DollarScriptletValue
import com.jessebrault.gcp.component.node.ExpressionScriptletValue
import com.jessebrault.gcp.component.node.GStringValue
import com.jessebrault.gcp.component.node.KeyAndValue
import com.jessebrault.gcp.component.node.KeysAndValues
import com.jessebrault.gcp.component.node.ComponentNode
import com.jessebrault.gcp.component.node.ScriptletValue
import com.jessebrault.gcp.component.node.StringValue
import com.jessebrault.gcp.util.PeekBefore
import groovy.transform.PackageScope
import static com.jessebrault.gcp.component.ComponentToken.Type.*
/**
* NOT thread safe
*/
@PackageScope
class ComponentParser {
private Queue<ComponentToken> tokens
private String currentIdentifier
ComponentRoot 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 ComponentRoot selfClosingComponent() {
this.startOfOpeningOrSelfClosingComponent()
def keysAndValues = this.keysAndValues()
this.expect(FORWARD_SLASH)
this.expect(GT)
new ComponentRoot().tap {
it.identifier = this.currentIdentifier
it.children << keysAndValues
}
}
private ComponentRoot openingComponent() {
this.startOfOpeningOrSelfClosingComponent()
def keysAndValues = this.keysAndValues()
this.expect(GT)
new ComponentRoot().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<ComponentNode> 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 ComponentNode value() {
if (this.peek(DOUBLE_QUOTE)) {
return this.doubleQuoteStringValue()
} else if (this.peek(SINGLE_QUOTE)) {
return this.singleQuoteStringValue()
} else if (this.peek(GROOVY_IDENTIFIER)) {
return this.dollarReferenceValue()
} else if (this.peek(GROOVY)) {
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, GROOVY_IDENTIFIER, GROOVY, 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([GROOVY_IDENTIFIER])
private DollarReferenceValue dollarReferenceValue() {
def groovyIdentifierToken = this.expect(GROOVY_IDENTIFIER)
new DollarReferenceValue().tap {
reference = groovyIdentifierToken.text
}
}
@PeekBefore([GROOVY])
private DollarScriptletValue dollarScriptletValue() {
def groovyToken = this.expect(GROOVY)
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
}
}
}

View File

@ -1,77 +0,0 @@
package com.jessebrault.gcp.component
import com.jessebrault.gcp.component.node.BooleanValue
import com.jessebrault.gcp.component.node.ComponentRoot
import com.jessebrault.gcp.component.node.DollarReferenceValue
import com.jessebrault.gcp.component.node.DollarScriptletValue
import com.jessebrault.gcp.component.node.ExpressionScriptletValue
import com.jessebrault.gcp.component.node.GStringValue
import com.jessebrault.gcp.component.node.KeyAndValue
import com.jessebrault.gcp.component.node.KeysAndValues
import com.jessebrault.gcp.component.node.ComponentNodeVisitor
import com.jessebrault.gcp.component.node.ScriptletValue
import com.jessebrault.gcp.component.node.StringValue
// NOT THREAD SAFE
class ComponentToClosureVisitor extends ComponentNodeVisitor {
private StringBuilder b = new StringBuilder()
String getResult() {
b.toString()
}
void reset() {
b = new StringBuilder()
}
void visit(ComponentRoot componentNode) {
b << '{ '
super.visit(componentNode)
if (componentNode.body != null) {
b << "bodyOut << ${ componentNode.body }; "
}
b << '};'
}
void visit(KeysAndValues keysAndValues) {
b << 'attr { '
super.visit(keysAndValues)
b << '}; '
}
void visit(KeyAndValue keyAndValue) {
b << "${ keyAndValue.key } = "
super.visit(keyAndValue)
b << '; '
}
void visit(GStringValue gStringValue) {
b << "\"${ gStringValue.gString }\""
}
void visit(StringValue stringValue) {
b << "'${ stringValue.string }'"
}
void visit(DollarReferenceValue dollarReferenceValue) {
b << dollarReferenceValue.reference
}
void visit(DollarScriptletValue dollarScriptletValue) {
b << dollarScriptletValue.scriptlet
}
void visit(ScriptletValue scriptletValue) {
b << "render { out -> ${ scriptletValue.scriptlet } }"
}
void visit(ExpressionScriptletValue expressionScriptletValue) {
b << expressionScriptletValue.scriptlet
}
void visit(BooleanValue booleanValue) {
b << booleanValue.value.toString()
}
}

View File

@ -1,37 +0,0 @@
package com.jessebrault.gcp.component
import groovy.transform.TupleConstructor
@TupleConstructor
class ComponentToken {
enum Type {
LT,
GT,
IDENTIFIER,
KEY,
EQUALS,
DOUBLE_QUOTE,
SINGLE_QUOTE,
STRING,
GROOVY,
GROOVY_IDENTIFIER,
PERCENT,
FORWARD_SLASH
;
boolean isAnyOf(Collection<Type> types) {
types.contains(this)
}
}
Type type
String text
@Override
String toString() {
"ComponentToken(${ this.type }, ${ this.text })"
}
}

View File

@ -1,159 +0,0 @@
package com.jessebrault.gcp.component
import com.jessebrault.fsm.function.FunctionFsmBuilder
import com.jessebrault.fsm.function.FunctionFsmBuilderImpl
import com.jessebrault.gcp.groovy.DollarScriptletParser
import com.jessebrault.gcp.util.PatternFunction
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 percent = new PatternFunction(~/^%/)
private static final PatternFunction expressionScriptletGroovy = new PatternFunction(~/^.*?%>/)
private static final PatternFunction forwardSlash = new PatternFunction(~/^\//)
static enum State {
START,
IDENTIFIER,
KEYS_AND_VALUES,
DOUBLE_QUOTE_STRING,
SINGLE_QUOTE_STRING,
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 shiftTo State.IDENTIFIER exec {
tokens << new ComponentToken(Type.LT, it)
}
onNoMatch() exec {
throw new IllegalArgumentException()
}
}
whileIn(State.IDENTIFIER) {
on identifier shiftTo State.KEYS_AND_VALUES exec {
tokens << new ComponentToken(Type.IDENTIFIER, it)
}
onNoMatch() exec {
throw new IllegalArgumentException()
}
}
whileIn(State.KEYS_AND_VALUES) {
on greaterThan shiftTo State.DONE exec {
tokens << new ComponentToken(Type.GT, 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.GROOVY_IDENTIFIER, s.substring(1)) // skip opening $
}
//noinspection GroovyAssignabilityCheck // for some reason IntelliJ is confused by this
on DollarScriptletParser::parse exec { String s ->
tokens << new ComponentToken(Type.GROOVY, s.substring(2, s.length() - 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.KEYS_AND_VALUES 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.KEYS_AND_VALUES exec {
tokens << new ComponentToken(Type.SINGLE_QUOTE, it)
}
onNoMatch() exec {
throw new IllegalArgumentException()
}
}
whileIn(State.EXPRESSION_SCRIPTLET_GROOVY) {
on expressionScriptletGroovy shiftTo State.KEYS_AND_VALUES 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
}
}

View File

@ -1,11 +0,0 @@
package com.jessebrault.gcp.component.node
class BooleanValue extends ComponentNode {
boolean value
@Override
String toString() {
"BooleanValue(${ this.value })"
}
}

View File

@ -1,5 +0,0 @@
package com.jessebrault.gcp.component.node
abstract class ComponentNode {
List<ComponentNode> children = []
}

View File

@ -1,15 +0,0 @@
package com.jessebrault.gcp.component.node
abstract class ComponentNodeVisitor {
void visit(ComponentNode node) {
this.visitChildren(node)
}
void visitChildren(ComponentNode node) {
node.children.each {
this.visit(it)
}
}
}

View File

@ -1,13 +0,0 @@
package com.jessebrault.gcp.component.node
class ComponentRoot extends ComponentNode {
String identifier
String body
@Override
String toString() {
"ComponentNode(identifier: ${ this.identifier }, body: ${ this.body }, children: ${ this.children })"
}
}

View File

@ -1,12 +0,0 @@
package com.jessebrault.gcp.component.node
class DollarReferenceValue extends ComponentNode {
String reference
@Override
String toString() {
"DollarReferenceValue(${ this.reference })"
}
}

View File

@ -1,12 +0,0 @@
package com.jessebrault.gcp.component.node
class DollarScriptletValue extends ComponentNode {
String scriptlet
@Override
String toString() {
"DollarScriptletValue(${ this.scriptlet })"
}
}

View File

@ -1,12 +0,0 @@
package com.jessebrault.gcp.component.node
class ExpressionScriptletValue extends ComponentNode {
String scriptlet
@Override
String toString() {
"ExpressionScriptletValue(${ this.scriptlet })"
}
}

View File

@ -1,12 +0,0 @@
package com.jessebrault.gcp.component.node
class GStringValue extends ComponentNode {
String gString
@Override
String toString() {
"GStringValue(${ this.gString })"
}
}

View File

@ -1,12 +0,0 @@
package com.jessebrault.gcp.component.node
class KeyAndValue extends ComponentNode {
String key
@Override
String toString() {
"KeyAndValue(key: ${ this.key }, children: ${ this.children })"
}
}

View File

@ -1,10 +0,0 @@
package com.jessebrault.gcp.component.node
class KeysAndValues extends ComponentNode {
@Override
String toString() {
"KeysAndValues(${ this.children })"
}
}

View File

@ -1,12 +0,0 @@
package com.jessebrault.gcp.component.node
class ScriptletValue extends ComponentNode {
String scriptlet
@Override
String toString() {
"ScriptletValue(${ this.scriptlet })"
}
}

View File

@ -1,12 +0,0 @@
package com.jessebrault.gcp.component.node
class StringValue extends ComponentNode {
String string
@Override
String toString() {
"StringValue(${ this.string })"
}
}

View File

@ -1,22 +0,0 @@
package com.jessebrault.gcp.groovy
class BlockScriptletParser {
static class Result {
String fullMatch
String scriptlet
}
static String parse(String input) {
}
static Result parseResult(String input) {
def match = parse(input)
match != null ? new Result().tap {
fullMatch = match
scriptlet = fullMatch.substring(2, fullMatch.length() - 2)
} : null
}
}

View File

@ -1,14 +0,0 @@
package com.jessebrault.gcp.groovy
class DollarReferenceParser {
static class Result {
String fullMatch
String reference
}
static Result parse(String input) {
}
}

View File

@ -1,146 +0,0 @@
package com.jessebrault.gcp.groovy
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class DollarScriptletParser {
private static Logger logger = LoggerFactory.getLogger(DollarScriptletParser)
static class Result {
String fullMatch
String scriptlet
}
private enum State {
NO_STRING, G_STRING, SINGLE_QUOTE_STRING
}
private static class Counter {
int count = 0
void increment() {
this.count++
}
void decrement() {
this.count--
}
void next() {
this.increment()
}
void previous() {
this.decrement()
}
boolean isZero() {
this.count == 0
}
@Override
String toString() {
"Counter(${ this.count })"
}
}
static String parse(String input) {
def acc = new StringBuilder()
def stateStack = new LinkedList<State>([State.NO_STRING])
def counterStack = new LinkedList<Counter>([new Counter()])
def iterator = input.iterator() as Iterator<String>
if (!iterator.hasNext() || iterator.next() != '$') {
return null
} else {
acc << '$'
counterStack.peek()++
}
if (!iterator.hasNext() || iterator.next() != '{') {
return null
} else {
acc << '{'
}
while (iterator.hasNext()) {
assert counterStack.size() > 0
assert stateStack.size() > 0
def c0 = iterator.next()
acc << c0
logger.debug('----')
logger.debug('c0: {}', c0)
logger.debug('acc: {}', acc)
if (stateStack.peek() == State.NO_STRING) {
if (c0 == '{') {
counterStack.peek()++
} else if (c0 == '}') {
counterStack.peek()--
if (counterStack.peek().isZero()) {
if (counterStack.size() == 1) {
logger.debug('single Counter is zero; breaking while loop')
break // escape while loop
} else {
logger.debug('counterStack.size() is greater than zero and top Counter is zero; ' +
'popping state and counter stacks')
counterStack.pop()
stateStack.pop()
}
}
} else if (c0 == '"') {
stateStack.push(State.G_STRING)
} else if (c0 == "'") {
stateStack.push(State.SINGLE_QUOTE_STRING)
}
} else if (stateStack.peek() == State.G_STRING) {
if (c0 == '\\') {
if (iterator.hasNext()) {
acc << iterator.next()
} else {
throw new IllegalArgumentException('Ill-formed dollar groovy')
}
} else if (c0 == '$') {
if (iterator.hasNext()) {
def c1 = iterator.next()
acc << c1
if (c1 == '{') {
stateStack.push(State.NO_STRING)
counterStack.push(new Counter())
counterStack.peek()++
}
} else {
throw new IllegalArgumentException('Ill-formed dollar groovy')
}
} else if (c0 == '"') {
logger.debug('popping G_STRING state')
stateStack.pop()
}
} else if (stateStack.peek() == State.SINGLE_QUOTE_STRING) {
if (c0 == "'") {
logger.debug('popping SINGLE_QUOTE_STRING state')
stateStack.pop()
}
}
logger.debug('stateStack: {}', stateStack)
logger.debug('counterStack: {}', counterStack)
}
acc.toString()
}
static Result parseResult(String input) {
def match = parse(input)
if (match) {
new Result().tap {
fullMatch = match
scriptlet = fullMatch.substring(2, fullMatch.length() - 1)
}
} else {
null
}
}
}

View File

@ -1,22 +0,0 @@
package com.jessebrault.gcp.groovy
class ExpressionScriptletParser {
static class Result {
String fullMatch
String scriptlet
}
static String parse(String input) {
}
static Result parseResult(String input) {
def match = parse(input)
match != null ? new Result().tap {
fullMatch = match
scriptlet = fullMatch.substring(3, fullMatch.length() - 2)
} : null
}
}

View File

@ -1,5 +0,0 @@
package com.jessebrault.gcp.node
class BlockScriptlet extends GcpNode {
String scriptlet
}

View File

@ -1,6 +0,0 @@
package com.jessebrault.gcp.node
class ComponentInstance extends GcpNode {
String opening
String closing
}

View File

@ -1,3 +0,0 @@
package com.jessebrault.gcp.node
class Document extends GcpNode {}

View File

@ -1,5 +0,0 @@
package com.jessebrault.gcp.node
class DollarReference extends GcpNode {
String reference
}

View File

@ -1,5 +0,0 @@
package com.jessebrault.gcp.node
class DollarScriptlet extends GcpNode {
String scriptlet
}

View File

@ -1,5 +0,0 @@
package com.jessebrault.gcp.node
class ExpressionScriptlet extends GcpNode {
String scriptlet
}

View File

@ -1,5 +0,0 @@
package com.jessebrault.gcp.node
abstract class GcpNode {
List<GcpNode> children = []
}

View File

@ -1,13 +0,0 @@
package com.jessebrault.gcp.node
abstract class GcpNodeVisitor {
void visit(GcpNode node) {
this.visitChildren(node)
}
void visitChildren(GcpNode node) {
node.children.each(this.&visit)
}
}

View File

@ -1,5 +0,0 @@
package com.jessebrault.gcp.node
class Html extends GcpNode {
String text
}

View File

@ -1,31 +0,0 @@
package com.jessebrault.gcp.tokenizer;
import java.util.Queue;
import java.util.regex.Pattern;
final class Accumulator {
private static final Pattern newline = Pattern.compile("([\n\r])");
private final Queue<Token> tokens;
private int inputIndex = 0;
private int line = 1;
private int col = 1;
public Accumulator(Queue<Token> tokenQueue) {
this.tokens = tokenQueue;
}
public void accumulate(Token.Type type, CharSequence text) {
this.tokens.add(new TokenImpl(type, text, this.inputIndex, this.line, this.col));
this.inputIndex += text.length();
final var m = newline.matcher(text);
if (m.find()) {
this.line += m.groupCount();
this.col = 1;
} else {
this.col += text.length();
}
}
}

View File

@ -1,24 +0,0 @@
package com.jessebrault.gcp.tokenizer;
final class Counter {
private int count = 0;
public void increment() {
this.count++;
}
public void decrement() {
this.count--;
}
public boolean isZero() {
return this.count == 0;
}
@Override
public String toString() {
return "Counter(" + this.count + ")";
}
}

View File

@ -1,205 +0,0 @@
package com.jessebrault.gcp.tokenizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.function.Supplier;
final class DollarScriptletMatcher implements FsmFunction {
private static final Logger logger = LoggerFactory.getLogger(DollarScriptletMatcher.class);
private static final class DollarScriptletMatcherOutput implements FsmOutput {
private final CharSequence entire;
private final String scriptlet;
public DollarScriptletMatcherOutput(
String entire,
String scriptlet
) {
this.entire = entire;
this.scriptlet = scriptlet;
}
@Override
public CharSequence entire() {
return this.entire;
}
@Override
public CharSequence part(int index) {
return switch (index) {
case 1 -> "$";
case 2 -> "{";
case 3 -> this.scriptlet;
case 4 -> "}";
default -> throw new IllegalArgumentException();
};
}
}
private enum State {
NO_STRING, G_STRING, SINGLE_QUOTE_STRING
}
private static final class CharSequenceIterator implements Iterator<String> {
private final CharSequence input;
private int cur;
public CharSequenceIterator(CharSequence input) {
this.input = input;
}
@Override
public boolean hasNext() {
return this.cur < input.length();
}
@Override
public String next() {
final var c = String.valueOf(input.charAt(this.cur));
this.cur++;
return c;
}
}
@Override
public FsmOutput apply(CharSequence s) {
final Deque<State> stateStack = new LinkedList<>();
final Deque<Counter> counterStack = new LinkedList<>();
final Supplier<Counter> currentCounterSupplier = () -> {
final var currentCounter = counterStack.peek();
if (currentCounter == null) {
throw new IllegalStateException("currentCounter is null");
}
return currentCounter;
};
stateStack.push(State.NO_STRING);
counterStack.push(new Counter());
final Iterator<String> iterator = new CharSequenceIterator(s);
final var entireAcc = new StringBuilder();
if (!iterator.hasNext() || !iterator.next().equals("$")) {
return null;
} else {
entireAcc.append("$");
}
if (!iterator.hasNext() || !iterator.next().equals("{")) {
return null;
} else {
entireAcc.append("{");
currentCounterSupplier.get().increment();
}
outer:
while (iterator.hasNext()) {
if (stateStack.isEmpty()) {
throw new IllegalStateException("stateStack is empty");
}
if (counterStack.isEmpty()) {
throw new IllegalStateException("counterStack is empty");
}
final var c0 = iterator.next();
entireAcc.append(c0);
logger.debug("----");
logger.debug("c0: {}", c0);
if (stateStack.peek() == State.NO_STRING) {
switch (c0) {
case "{" -> currentCounterSupplier.get().increment();
case "}" -> {
final var currentCounter = currentCounterSupplier.get();
currentCounter.decrement();
if (currentCounter.isZero()) {
if (counterStack.size() == 1) {
logger.debug("last Counter is zero; breaking while loop");
break outer;
} else {
logger.debug("counterStack.size() is greater than 1 and top Counter is zero; " +
"popping state and counter stacks.");
stateStack.pop();
counterStack.pop();
}
}
}
case "\"" -> stateStack.push(State.G_STRING);
case "'" -> stateStack.push(State.SINGLE_QUOTE_STRING);
}
} else if (stateStack.peek() == State.G_STRING) {
switch (c0) {
case "\\" -> {
if (iterator.hasNext()) {
final var c1 = iterator.next();
entireAcc.append(c1);
} else {
throw new IllegalArgumentException(
"Ill-formed dollarScriptlet (backslash followed by nothing)"
);
}
}
case "$" -> {
if (iterator.hasNext()) {
final var c1 = iterator.next();
entireAcc.append(c1);
if (c1.equals("{")) {
stateStack.push(State.NO_STRING);
counterStack.push(new Counter());
currentCounterSupplier.get().increment();
}
} else {
throw new IllegalArgumentException("Ill-formed dollarScriptlet (ends with a dollar)");
}
}
case "\"" -> {
logger.debug("popping G_STRING state");
stateStack.pop();
}
}
} else if (stateStack.peek() == State.SINGLE_QUOTE_STRING) {
switch (c0) {
case "\\" -> {
if (iterator.hasNext()) {
entireAcc.append(iterator.next());
} else {
throw new IllegalArgumentException(
"Ill-formed dollarScriptlet (backslash followed by nothing)"
);
}
}
case "'" -> {
logger.debug("popping SINGLE_QUOTE_STRING state");
stateStack.pop();
}
}
} else {
throw new IllegalStateException(
"stateStack contains something which does not equal a state or is null"
);
}
logger.debug("entireAcc: {}", entireAcc);
logger.debug("stateStack: {}", stateStack);
logger.debug("counterStack: {}", counterStack);
}
return new DollarScriptletMatcherOutput(
entireAcc.toString(),
entireAcc.substring(2, entireAcc.length() - 1)
);
}
}

View File

@ -1,5 +0,0 @@
package com.jessebrault.gcp.tokenizer;
import java.util.function.Function;
interface FsmFunction extends Function<CharSequence, FsmOutput> {}

View File

@ -1,6 +0,0 @@
package com.jessebrault.gcp.tokenizer;
interface FsmOutput {
CharSequence entire();
CharSequence part(int index);
}

View File

@ -1,108 +0,0 @@
package com.jessebrault.gcp.tokenizer;
import com.jessebrault.fsm.stackfunction.StackFunctionFsm;
import com.jessebrault.fsm.stackfunction.StackFunctionFsmBuilder;
import com.jessebrault.fsm.stackfunction.StackFunctionFsmBuilderImpl;
import java.util.regex.Pattern;
final class GStringMatcher implements FsmFunction {
private static final class GStringMatcherOutput implements FsmOutput {
private final CharSequence entire;
private final CharSequence contents;
public GStringMatcherOutput(String entire, String contents) {
this.entire = entire;
this.contents = contents;
}
@Override
public CharSequence entire() {
return this.entire;
}
@Override
public CharSequence part(int index) {
return switch(index) {
case 1, 3 -> "\"";
case 2 -> this.contents;
default -> throw new IllegalArgumentException();
};
}
}
private static final PatternMatcher text = new PatternMatcher(
Pattern.compile("^(?:[\\w\\W&&[^$\\\\\"\\n\\r]]|\\\\[\"nrbfst\\\\u]|\\$(?!\\{|[\\w$]+(?:\\.[\\w$]+)*))+")
);
private static final DollarScriptletMatcher dollarScriptlet = new DollarScriptletMatcher();
private static final PatternMatcher doubleQuote = new PatternMatcher(
Pattern.compile("^\"")
);
private enum State {
START, CONTENTS, DONE
}
private static StackFunctionFsmBuilder<CharSequence, State, FsmOutput> getFsmBuilder() {
return new StackFunctionFsmBuilderImpl<>();
}
private static StackFunctionFsm<CharSequence, State, FsmOutput> getFsm(StringBuilder acc) {
return getFsmBuilder()
.setInitialState(State.START)
.whileIn(State.START, sc -> {
sc.on(doubleQuote).shiftTo(State.CONTENTS).exec(o -> {
acc.append(o.entire());
});
sc.onNoMatch().exec(input -> {
throw new IllegalArgumentException();
});
})
.whileIn(State.CONTENTS, sc -> {
sc.on(text).exec(o -> {
acc.append(o.entire());
});
sc.on(dollarScriptlet).exec(o -> {
acc.append(o.entire());
});
sc.on(doubleQuote).shiftTo(State.DONE).exec(o -> {
acc.append(o.entire());
});
sc.onNoMatch().exec(input -> {
throw new IllegalArgumentException();
});
})
.build();
}
@Override
public FsmOutput apply(final CharSequence s) {
final var acc = new StringBuilder();
final var fsm = getFsm(acc);
CharSequence remaining = s;
// Look-ahead
if (!String.valueOf(remaining.charAt(0)).equals("\"")) {
return null;
}
while (remaining.length() > 0) {
final var output = fsm.apply(remaining);
if (output == null) {
throw new IllegalStateException("output is null");
}
if (fsm.getCurrentState() == State.DONE) {
break;
}
remaining = remaining.subSequence(output.entire().length(), remaining.length());
}
final var entire = acc.toString();
return new GStringMatcherOutput(entire, entire.substring(1, entire.length() - 1));
}
}

View File

@ -1,50 +0,0 @@
package com.jessebrault.gcp.tokenizer;
import java.util.regex.MatchResult;
import java.util.regex.Pattern;
final class PatternMatcher implements FsmFunction {
private static final class MatchResultFsmOutput implements FsmOutput {
private final MatchResult matchResult;
public MatchResultFsmOutput(MatchResult matchResult) {
this.matchResult = matchResult;
}
@Override
public CharSequence entire() {
return this.matchResult.group(0);
}
@Override
public CharSequence part(int index) {
return this.matchResult.group(index);
}
@Override
public String toString() {
return "MatchResultFsmOutput(" + this.entire() + ")";
}
}
private final Pattern pattern;
public PatternMatcher(Pattern pattern) {
this.pattern = pattern;
}
@Override
public FsmOutput apply(CharSequence s) {
final var m = this.pattern.matcher(s);
return m.find() ? new MatchResultFsmOutput(m) : null;
}
@Override
public String toString() {
return "MatcherFunction(" + this.pattern + ")";
}
}

View File

@ -1,49 +0,0 @@
package com.jessebrault.gcp.tokenizer;
public final class TokenImpl implements Token {
private final Type type;
private final CharSequence text;
private final int inputIndex;
private final int line;
private final int col;
public TokenImpl(Type type, CharSequence text, int inputIndex, int line, int col) {
this.type = type;
this.text = text;
this.inputIndex = inputIndex;
this.line = line;
this.col = col;
}
@Override
public Type getType() {
return type;
}
@Override
public CharSequence getText() {
return text;
}
@Override
public int getInputIndex() {
return 0;
}
@Override
public int getLine() {
return line;
}
@Override
public int getCol() {
return col;
}
@Override
public String toString() {
return String.format("Token(%s, %s, %d, %d, %d)", this.type, this.text, this.inputIndex, this.line, this.col);
}
}

View File

@ -1,195 +0,0 @@
package com.jessebrault.gcp.tokenizer;
import com.jessebrault.fsm.function.FunctionFsm;
import com.jessebrault.fsm.function.FunctionFsmBuilder;
import com.jessebrault.fsm.function.FunctionFsmBuilderImpl;
import static com.jessebrault.gcp.tokenizer.Token.Type.*;
import java.util.regex.Pattern;
final class TokenizerFsm {
/**
* Text
*/
private static final FsmFunction text = new PatternMatcher(
Pattern.compile("^(?:[\\w\\W&&[^<$]]|<(?!%|/?\\p{Lu}|/?[\\p{L}0-9_$]+(?:\\.[\\p{L}0-9_$]+)+)|\\$(?![\\w$]+(?:\\.[\\w$]+)*))+")
);
/**
* Gsp dollar reference and scriptlets, also used as component values
*/
private static final FsmFunction dollarReference = new PatternMatcher(
Pattern.compile("^(\\$)([\\w$]+(?:\\.[\\w$]+)*)")
);
private static final FsmFunction dollarScriptlet = new DollarScriptletMatcher();
private static final FsmFunction blockScriptlet = new PatternMatcher(
Pattern.compile("^(<%)(.*?)(%>)")
);
private static final FsmFunction expressionScriptlet = new PatternMatcher(
Pattern.compile("^(<%=)(.*?)(%>)")
);
/**
* Component starts
*/
private static final FsmFunction openingComponentStart = new PatternMatcher(
Pattern.compile("^<(?=\\p{Lu}|[\\p{L}0-9_$]+(?:\\.[\\p{L}0-9_$]+)+)")
);
private static final FsmFunction closingComponentStart = new PatternMatcher(
Pattern.compile("^(<)(/)(?=\\p{Lu}|[\\p{L}0-9_$]+(?:\\.[\\p{L}0-9_$]+)+)")
);
/**
* Component names
*/
private static final FsmFunction className = new PatternMatcher(
Pattern.compile("^\\p{Lu}[\\p{L}0-9_$]*")
);
private static final FsmFunction packageName = new PatternMatcher(
Pattern.compile("^[\\p{L}0-9_$]+(?=\\.)")
);
private static final FsmFunction dot = new PatternMatcher(
Pattern.compile("^\\.")
);
/**
* Whitespace
*/
private static final FsmFunction whitespace = new PatternMatcher(Pattern.compile("^\\s+"));
/**
* Keys and values
*/
private static final FsmFunction key = new PatternMatcher(
Pattern.compile("^[\\p{L}0-9_$]+")
);
private static final FsmFunction equals = new PatternMatcher(Pattern.compile("^="));
private static final FsmFunction singleQuoteString = new PatternMatcher(
Pattern.compile("^(')((?:[\\w\\W&&[^\\\\'\\n\\r]]|\\\\['nrbfst\\\\u])*)(')")
);
private static final FsmFunction gString = new GStringMatcher();
/**
* Component ends
*/
private static final FsmFunction forwardSlash = new PatternMatcher(Pattern.compile("^/"));
private static final FsmFunction componentEnd = new PatternMatcher(Pattern.compile("^>"));
private static FunctionFsmBuilder<CharSequence, Tokenizer.State, FsmOutput> getFsmBuilder() {
return new FunctionFsmBuilderImpl<>();
}
public static FunctionFsm<CharSequence, Tokenizer.State, FsmOutput> get(Accumulator acc, Tokenizer.State state) {
return getFsmBuilder()
.setInitialState(state)
.whileIn(Tokenizer.State.TEXT, sc -> {
sc.on(text).exec(o -> {
acc.accumulate(TEXT, o.entire());
});
sc.on(dollarReference).exec(o -> {
acc.accumulate(DOLLAR, o.part(1));
acc.accumulate(GROOVY_REFERENCE, o.part(2));
});
sc.on(dollarScriptlet).exec(o -> {
acc.accumulate(DOLLAR, o.part(1));
acc.accumulate(CURLY_OPEN, o.part(2));
acc.accumulate(SCRIPTLET, o.part(3));
acc.accumulate(CURLY_CLOSE, o.part(4));
});
sc.on(blockScriptlet).exec(o -> {
acc.accumulate(BLOCK_SCRIPTLET_OPEN, o.part(1));
acc.accumulate(SCRIPTLET, o.part(2));
acc.accumulate(SCRIPTLET_CLOSE, o.part(3));
});
sc.on(expressionScriptlet).exec(o -> {
acc.accumulate(EXPRESSION_SCRIPTLET_OPEN, o.part(1));
acc.accumulate(SCRIPTLET, o.part(2));
acc.accumulate(SCRIPTLET_CLOSE, o.part(3));
});
sc.on(openingComponentStart).shiftTo(Tokenizer.State.COMPONENT_NAME).exec(o ->
acc.accumulate(COMPONENT_START, o.entire())
);
sc.on(closingComponentStart).shiftTo(Tokenizer.State.COMPONENT_NAME).exec(o -> {
acc.accumulate(COMPONENT_START, o.part(1));
acc.accumulate(FORWARD_SLASH, o.part(2));
});
sc.onNoMatch().exec(input -> { throw new IllegalArgumentException(); });
})
.whileIn(Tokenizer.State.COMPONENT_NAME, sc -> {
sc.on(packageName).exec(o -> {
acc.accumulate(PACKAGE_NAME, o.entire());
});
sc.on(dot).exec(o -> {
acc.accumulate(DOT, o.entire());
});
sc.on(className).exec(o -> {
acc.accumulate(CLASS_NAME, o.entire());
});
sc.on(forwardSlash).exec(o -> {
acc.accumulate(FORWARD_SLASH, o.entire());
});
sc.on(componentEnd).shiftTo(Tokenizer.State.TEXT).exec(o -> {
acc.accumulate(COMPONENT_END, o.entire());
});
sc.on(whitespace).shiftTo(Tokenizer.State.COMPONENT_KEYS_AND_VALUES).exec(o -> {
acc.accumulate(WHITESPACE, o.entire());
});
sc.onNoMatch().exec(input -> { throw new IllegalArgumentException(); });
})
.whileIn(Tokenizer.State.COMPONENT_KEYS_AND_VALUES, sc -> {
sc.on(componentEnd).shiftTo(Tokenizer.State.TEXT).exec(o -> {
acc.accumulate(COMPONENT_END, o.entire());
});
sc.on(whitespace).exec(o -> {
acc.accumulate(WHITESPACE, o.entire());
});
sc.on(key).exec(o -> {
acc.accumulate(KEY, o.entire());
});
sc.on(equals).exec(o -> {
acc.accumulate(EQUALS, o.entire());
});
sc.on(gString).exec(o -> {
acc.accumulate(DOUBLE_QUOTE, o.part(1));
acc.accumulate(STRING, o.part(2));
acc.accumulate(DOUBLE_QUOTE, o.part(3));
});
sc.on(singleQuoteString).exec(o -> {
acc.accumulate(SINGLE_QUOTE, o.part(1));
acc.accumulate(STRING, o.part(2));
acc.accumulate(SINGLE_QUOTE, o.part(3));
});
sc.on(dollarReference).exec(o -> {
acc.accumulate(DOLLAR, o.part(1));
acc.accumulate(GROOVY_REFERENCE, o.part(2));
});
sc.on(dollarScriptlet).exec(o -> {
acc.accumulate(DOLLAR, o.part(1));
acc.accumulate(CURLY_OPEN, o.part(2));
acc.accumulate(SCRIPTLET, o.part(3));
acc.accumulate(CURLY_CLOSE, o.part(4));
});
sc.on(blockScriptlet).exec(o -> {
acc.accumulate(BLOCK_SCRIPTLET_OPEN, o.part(1));
acc.accumulate(SCRIPTLET, o.part(2));
acc.accumulate(SCRIPTLET_CLOSE, o.part(3));
});
sc.on(expressionScriptlet).exec(o -> {
acc.accumulate(EXPRESSION_SCRIPTLET_OPEN, o.part(1));
acc.accumulate(SCRIPTLET, o.part(2));
acc.accumulate(SCRIPTLET_CLOSE, o.part(3));
});
sc.on(forwardSlash).exec(o -> {
acc.accumulate(FORWARD_SLASH, o.entire());
});
sc.on(componentEnd).shiftTo(Tokenizer.State.TEXT).exec(o -> {
acc.accumulate(COMPONENT_END, o.entire());
});
sc.onNoMatch().exec(input -> { throw new IllegalArgumentException(); });
})
.build();
}
}

View File

@ -1,62 +0,0 @@
package com.jessebrault.gcp.tokenizer;
import com.jessebrault.fsm.function.FunctionFsm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.LinkedList;
import java.util.Queue;
public final class TokenizerImpl implements Tokenizer {
private static final Logger logger = LoggerFactory.getLogger(TokenizerImpl.class);
private CharSequence input;
private int currentOffset;
private int endOffset;
private Queue<Token> tokens;
private FunctionFsm<CharSequence, State, FsmOutput> fsm;
@Override
public void start(CharSequence input, int startOffset, int endOffset, State initialState) {
this.input = input;
this.currentOffset = startOffset;
this.endOffset = endOffset;
this.tokens = new LinkedList<>();
this.fsm = TokenizerFsm.get(new Accumulator(this.tokens), initialState);
}
@Override
public boolean hasNext() {
if (this.tokens.isEmpty()) {
this.getNextTokens();
}
return !this.tokens.isEmpty();
}
private void getNextTokens() {
if (this.currentOffset != this.endOffset) {
final var match = this.fsm.apply(this.input.subSequence(this.currentOffset, this.endOffset));
if (match == null) {
logger.error("match is null!");
} else {
this.currentOffset += match.entire().length();
}
}
}
@Override
public Token next() {
if (this.tokens.isEmpty()) {
throw new IllegalStateException("currentAccumulatedTokens is empty");
}
return this.tokens.remove();
}
@Override
public State getCurrentState() {
return this.fsm.getCurrentState();
}
}

View File

@ -1,25 +0,0 @@
package com.jessebrault.gcp.util
import groovy.transform.PackageScope
import groovy.transform.TupleConstructor
import java.util.function.Function
import java.util.regex.Pattern
@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 })"
}
}

View File

@ -1,11 +0,0 @@
package com.jessebrault.gcp.util
import com.jessebrault.gcp.component.ComponentToken
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
@Retention(RetentionPolicy.SOURCE)
@interface PeekBefore {
ComponentToken.Type[] value()
}

View File

@ -1 +0,0 @@
com.jessebrault.gcp.GcpToScriptConverterImpl

View File

@ -1,6 +0,0 @@
def ctx = context(filetypes: ['gsp'])
contributor(ctx) {
method name: 'foo', params: [bar: 'String'], type: 'int'
property name: 'texts', type: 'java.util.List<String>', doc: 'Some texts.'
}

View File

@ -1 +0,0 @@

View File

@ -1,5 +0,0 @@
def elf = foo('elf')
texts.each {
}

View File

@ -1,137 +0,0 @@
package com.jessebrault.gcp.component
import com.jessebrault.gcp.component.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.gcp.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 ComponentNode> {
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(ComponentNode 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 ComponentNode>> childSpecs = []
def <T extends ComponentNode> void expect(
Class<T> childNodeClass,
@DelegatesTo(value = NodeTester, strategy = Closure.DELEGATE_FIRST)
@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(
Queue<ComponentToken> tokens,
@DelegatesTo(value = NodeTester, strategy = Closure.DELEGATE_FIRST)
@ClosureParams(value = SimpleType, options = ['com.jessebrault.gcp.component.node.ComponentNode'])
Closure<Void> tests
) {
def componentNode = this.parser.parse(tokens)
logger.debug('componentNode: {}', componentNode)
def componentSpec = new NodeSpec(ComponentRoot, tests)
logger.debug('nodeSpec: {}', componentSpec)
componentSpec.test(componentNode)
}
@Test
void selfClosingNoKeysOrValues() {
this.selfClosing(new LinkedList<>([
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 LinkedList<>([
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)
}
}
}
}
}
}

View File

@ -1,40 +0,0 @@
package com.jessebrault.gcp.component
import com.jessebrault.gcp.component.node.ComponentRoot
import com.jessebrault.gcp.component.node.GStringValue
import com.jessebrault.gcp.component.node.KeyAndValue
import com.jessebrault.gcp.component.node.KeysAndValues
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
class ComponentToClosureVisitorTests {
@Test
void withEmptyKeysAndValues() {
def cn = new ComponentRoot().tap {
it.children << new KeysAndValues()
}
def v = new ComponentToClosureVisitor()
v.visit(cn)
assertEquals('{ attr { }; };', v.result)
}
@Test
void withGStringKeyAndValue() {
def cn = new ComponentRoot().tap {
it.children << new KeysAndValues().tap {
it.children << new KeyAndValue().tap {
key = 'greeting'
it.children << new GStringValue().tap {
gString = 'Hello, ${ frontMatter.person }!'
}
}
}
}
def v = new ComponentToClosureVisitor()
v.visit(cn)
assertEquals('{ attr { greeting = "Hello, ${ frontMatter.person }!"; }; };', v.result)
}
}

View File

@ -1,158 +0,0 @@
package com.jessebrault.gcp.component
import org.junit.jupiter.api.Test
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import static com.jessebrault.gcp.component.ComponentToken.Type.*
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertTrue
class ComponentTokenizerTests {
private static final Logger logger = LoggerFactory.getLogger(ComponentTokenizerTests)
private static class TokenSpec {
ComponentToken.Type type
String text
TokenSpec(ComponentToken.Type type, String text = null) {
this.type = Objects.requireNonNull(type)
this.text = text
}
void compare(ComponentToken actual) {
assertEquals(this.type, actual.type)
if (this.text != null) {
assertEquals(this.text, actual.text)
}
}
@Override
String toString() {
"TokenSpec(${ this.type }, ${ this.text })"
}
}
private static class TesterConfigurator {
Queue<TokenSpec> specs = new LinkedList<>()
void expect(ComponentToken.Type type, String text) {
this.specs << new TokenSpec(type, text)
}
void expect(ComponentToken.Type type) {
this.specs << new TokenSpec(type)
}
}
private final ComponentTokenizer tokenizer = new ComponentTokenizer()
private void test(
String src,
@DelegatesTo(value = TesterConfigurator, strategy = Closure.DELEGATE_FIRST)
Closure<Void> configure
) {
def configurator = new TesterConfigurator()
configure.setDelegate(configurator)
configure.setResolveStrategy(Closure.DELEGATE_FIRST)
configure()
def r = this.tokenizer.tokenize(src)
logger.debug('r: {}', r)
logger.debug('configurator.specs: {}', configurator.specs)
assertEquals(configurator.specs.size(), r.size())
def resultIterator = r.iterator()
configurator.specs.each {
assertTrue(resultIterator.hasNext())
it.compare(resultIterator.next())
}
}
@Test
void selfClosingComponent() {
this.test('<Test />') {
expect LT
expect IDENTIFIER, 'Test'
expect FORWARD_SLASH
expect GT
}
}
@Test
void selfClosingComponentWithDoubleQuotedString() {
this.test('<Test key="value" />') {
expect LT
expect IDENTIFIER, 'Test'
expect KEY, 'key'
expect EQUALS
expect DOUBLE_QUOTE
expect STRING, 'value'
expect DOUBLE_QUOTE
expect FORWARD_SLASH
expect GT
}
}
@Test
void selfClosingComponentWithSingleQuotedString() {
this.test("<Test key='value' />") {
expect LT
expect IDENTIFIER, 'Test'
expect KEY, 'key'
expect EQUALS
expect SINGLE_QUOTE
expect STRING, 'value'
expect SINGLE_QUOTE
expect FORWARD_SLASH
expect GT
}
}
@Test
void componentWithSimpleDollarGroovy() {
this.test('<Test key=${ test } />') {
expect LT
expect IDENTIFIER, 'Test'
expect KEY, 'key'
expect EQUALS
expect GROOVY, ' test '
expect FORWARD_SLASH
expect GT
}
}
@Test
void dollarGroovyNestedBraces() {
this.test('<Test key=${ test.each { it.test() } } />') {
expect LT
expect IDENTIFIER, 'Test'
expect KEY, 'key'
expect EQUALS
expect GROOVY, ' test.each { it.test() } '
expect FORWARD_SLASH
expect GT
}
}
@Test
void dollarReference() {
this.test('<Test key=$test />') {
expect LT
expect IDENTIFIER, 'Test'
expect KEY, 'key'
expect EQUALS
expect GROOVY_IDENTIFIER, 'test'
expect FORWARD_SLASH
expect GT
}
}
}

View File

@ -1,65 +0,0 @@
package com.jessebrault.gcp.groovy
import org.junit.jupiter.api.Test
import static com.jessebrault.gcp.groovy.DollarScriptletParser.parse
import static org.junit.jupiter.api.Assertions.assertEquals
class DollarScriptletParserTests {
@Test
void empty() {
assertEquals('${}', parse('${}'))
}
@Test
void simple() {
assertEquals('${ 1 + 2 }', parse('${ 1 + 2 }'))
}
@Test
void nestedString() {
assertEquals('${ "myString" }', parse('${ "myString" }'))
}
@Test
void nestedCurlyBraces() {
assertEquals(
'${ [1, 2, 3].collect { it + 1 }.size() }',
parse('${ [1, 2, 3].collect { it + 1 }.size() }')
)
}
@Test
void nestedSingleQuoteString() {
assertEquals(
'${ \'abc\' }',
parse('${ \'abc\' }')
)
}
@Test
void nestedGString() {
assertEquals(
'${ "abc" }',
parse('${ "abc" }')
)
}
@Test
void nestedGStringWithClosure() {
assertEquals(
'${ "abc${ it }" }',
parse('${ "abc${ it }" }')
)
}
@Test
void takesOnlyAsNeeded() {
assertEquals(
'${ 1 + 2 }',
parse('${ 1 + 2 } someOther=${ 3 + 4 }')
)
}
}

View File

@ -1,60 +0,0 @@
package com.jessebrault.gcp.tokenizer
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
class DollarScriptletMatcherTests {
private final DollarScriptletMatcher matcher = new DollarScriptletMatcher();
private void test(String expectedEntire, String input) {
def r = this.matcher.apply(input)
assertEquals(expectedEntire, r.entire())
assertEquals('$', r.part(1))
assertEquals('{', r.part(2))
assertEquals(expectedEntire.substring(2, expectedEntire.length() - 1), r.part(3))
assertEquals('}', r.part(4))
}
@Test
void empty() {
test '${}', '${}'
}
@Test
void simple() {
test '${ 1 + 2 }', '${ 1 + 2 }'
}
@Test
void nestedString() {
test '${ "myString" }', '${ "myString" }'
}
@Test
void nestedCurlyBraces() {
test '${ [1, 2, 3].collect { it + 1 }.size() }', '${ [1, 2, 3].collect { it + 1 }.size() }'
}
@Test
void nestedSingleQuoteString() {
test '${ \'abc\' }', '${ \'abc\' }'
}
@Test
void nestedGString() {
test '${ "abc" }', '${ "abc" }'
}
@Test
void nestedGStringWithClosure() {
test '${ "abc${ it }" }', '${ "abc${ it }" }'
}
@Test
void takesOnlyAsNeeded() {
test '${ 1 + 2 }', '${ 1 + 2 } someOther=${ 3 + 4 }'
}
}

View File

@ -1,44 +0,0 @@
package com.jessebrault.gcp.tokenizer
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
class GStringMatcherTests {
private final GStringMatcher matcher = new GStringMatcher()
private void test(String expectedEntire, String input) {
def output = this.matcher.apply(input)
assertEquals(expectedEntire, output.entire())
assertEquals('"', output.part(1))
assertEquals(expectedEntire.substring(1, expectedEntire.length() - 1), output.part(2))
assertEquals('"', output.part(3))
}
@Test
void empty() {
test '""', '""'
}
@Test
void simple() {
test '"abc"', '"abc"'
}
@Test
void nestedDollarClosureWithGString() {
test '"abc ${ \'def\'.each { "$it " }.join() }"', '"abc ${ \'def\'.each { "$it " }.join() }"'
}
@Test
void nestedDollarClosureWithGStringTakesOnlyAsNeeded() {
test '"abc ${ \'def\'.each { "$it " }.join() }"', '"abc ${ \'def\'.each { "$it " }.join() }" test="rest"'
}
@Test
void takesOnlyAsNeeded() {
test '"abc"', '"abc" test="def"'
}
}

View File

@ -1,196 +0,0 @@
package com.jessebrault.gcp.tokenizer
import org.junit.jupiter.api.Test
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import static com.jessebrault.gcp.tokenizer.Token.Type.*
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertTrue
class TokenizerTests {
private static final Logger logger = LoggerFactory.getLogger(TokenizerTests)
private static class TokenSpec {
Token.Type type
String text
int line
int col
TokenSpec(Token.Type type, String text = null, line = 0, col = 0) {
this.type = Objects.requireNonNull(type)
this.text = text
this.line = line
this.col = col
}
void compare(Token actual) {
assertEquals(this.type, actual.type)
if (this.text != null) {
assertEquals(this.text, actual.text)
}
if (this.line != 0) {
assertEquals(this.line, actual.line)
}
if (this.col != 0) {
assertEquals(this.col, actual.col)
}
}
@Override
String toString() {
"TokenSpec(${ this.type }, ${ this.text }, ${ this.line }, ${ this.col })"
}
}
private static class TesterConfigurator {
Queue<TokenSpec> specs = new LinkedList<>()
void expect(Token.Type type, String text = null, line = 0, col = 0) {
this.specs << new TokenSpec(type, text, line, col)
}
}
private static void test(
String src,
@DelegatesTo(value = TesterConfigurator, strategy = Closure.DELEGATE_FIRST)
Closure<Void> configure
) {
def configurator = new TesterConfigurator()
configure.setDelegate(configurator)
configure.setResolveStrategy(Closure.DELEGATE_FIRST)
configure()
def r = new TokenizerImpl().tokenizeAll(src, Tokenizer.State.TEXT)
logger.debug('r: {}', r)
logger.debug('configurator.specs: {}', configurator.specs)
assertEquals(configurator.specs.size(), r.size())
def resultIterator = r.iterator()
configurator.specs.each {
assertTrue(resultIterator.hasNext())
it.compare(resultIterator.next())
}
}
@Test
void doctypeHtmlIsText() {
test('<!DOCTYPE html>') {
expect TEXT, '<!DOCTYPE html>', 1, 1
}
}
@Test
void htmlLangEnIsText() {
test('<html lang="en">') {
expect TEXT, '<html lang="en">', 1, 1
}
}
@Test
void component() {
test('<Test />') {
expect COMPONENT_START, '<', 1, 1
expect CLASS_NAME, 'Test', 1, 2
expect WHITESPACE, ' ', 1, 6
expect FORWARD_SLASH, '/', 1, 7
expect COMPONENT_END, '>', 1, 8
}
}
@Test
void componentWithGString() {
test('<Test test="test" />') {
expect COMPONENT_START, '<', 1, 1
expect CLASS_NAME, 'Test', 1, 2
expect WHITESPACE, ' ', 1, 6
expect KEY, 'test', 1, 7
expect EQUALS, '=', 1, 11
expect DOUBLE_QUOTE, '"', 1, 12
expect STRING, 'test', 1, 13
expect DOUBLE_QUOTE, '"', 1, 17
expect WHITESPACE, ' ', 1, 18
expect FORWARD_SLASH, '/', 1, 19
expect COMPONENT_END, '>', 1, 20
}
}
@Test
void componentWithGStringWithNestedGString() {
test('<Test test="abc ${ \'abc\'.collect { "it " }.join() }" />') {
expect COMPONENT_START, '<', 1, 1
expect CLASS_NAME, 'Test', 1, 2
expect WHITESPACE, ' ', 1, 6
expect KEY, 'test', 1, 7
expect EQUALS, '=', 1, 11
expect DOUBLE_QUOTE, '"', 1, 12
expect STRING, 'abc ${ \'abc\'.collect { "it " }.join() }', 1, 13
expect DOUBLE_QUOTE, '"', 1, 52
expect WHITESPACE, ' ', 1, 53
expect FORWARD_SLASH, '/', 1, 54
expect COMPONENT_END, '>', 1, 55
}
}
@Test
void newlinesCounted() {
test('Hello,\n$person!') {
expect TEXT, 'Hello,\n', 1, 1
expect DOLLAR, '$', 2, 1
expect GROOVY_REFERENCE, 'person', 2, 2
expect TEXT, '!', 2, 8
}
}
@Test
void componentWithSingleQuoteString() {
test("<Test test='Hello, World!' />") {
expect COMPONENT_START, '<', 1, 1
expect CLASS_NAME, 'Test', 1, 2
expect WHITESPACE, ' ', 1, 6
expect KEY, 'test', 1, 7
expect EQUALS, '=', 1, 11
expect SINGLE_QUOTE, "'", 1, 12
expect STRING, 'Hello, World!', 1, 13
expect SINGLE_QUOTE, "'", 1, 26
expect WHITESPACE, ' ', 1, 27
expect FORWARD_SLASH, '/', 1, 28
expect COMPONENT_END, '>', 1, 29
}
}
@Test
void componentWithFullyQualifiedName() {
test('<com.jessebrault.gcp.Test />') {
expect COMPONENT_START, '<', 1, 1
expect PACKAGE_NAME, 'com', 1, 2
expect DOT, '.', 1, 5
expect PACKAGE_NAME, 'jessebrault', 1, 6
expect DOT, '.', 1, 17
expect PACKAGE_NAME, 'gcp', 1, 18
expect DOT, '.', 1, 21
expect CLASS_NAME, 'Test', 1, 22
expect WHITESPACE, ' ', 1, 26
expect FORWARD_SLASH, '/', 1, 27
expect COMPONENT_END, '>', 1, 28
}
}
@Test
void componentWithNewlineWhitespace() {
test('<Test\n/>') {
expect COMPONENT_START, '<', 1, 1
expect CLASS_NAME, 'Test', 1, 2
expect WHITESPACE, '\n', 1, 6
expect FORWARD_SLASH, '/', 2, 1
expect COMPONENT_END, '>', 2, 2
}
}
}

View File

@ -1,17 +0,0 @@
<?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>

View File

@ -1,2 +1,2 @@
rootProject.name = 'ssg'
include 'cli', 'gcp-api', 'gcp-impl', 'lib'
include 'cli', 'lib'