diff --git a/web-views/build.gradle b/web-views/build.gradle index ea317ee..83146cb 100644 --- a/web-views/build.gradle +++ b/web-views/build.gradle @@ -144,11 +144,12 @@ def toolSpec = { String name, String mainClass -> } final List toolSpecs = [ - toolSpec('parseTreeFileMaker', 'ParseTreeFileMakerCli'), toolSpec('astFileMaker', 'AstFileMakerCli'), toolSpec('convertToGroovy', 'ConvertToGroovy'), toolSpec('lexer', 'LexerTool'), - toolSpec('parser', 'ParserTool') + toolSpec('parser', 'ParserTool'), + toolSpec('parseTreeFileMaker', 'ParseTreeFileMakerCli'), + toolSpec('runTemplate', 'RunTemplate') ] toolSpecs.each { spec -> diff --git a/web-views/sketching/.gitignore b/web-views/sketching/.gitignore index c1f99c7..e2f79fc 100644 --- a/web-views/sketching/.gitignore +++ b/web-views/sketching/.gitignore @@ -1,2 +1,3 @@ *.groovy classes +.txt diff --git a/web-views/sketching/preambleHelloTarget.wvc b/web-views/sketching/preambleHelloTarget.wvc index bc9b1fd..0d0cc38 100644 --- a/web-views/sketching/preambleHelloTarget.wvc +++ b/web-views/sketching/preambleHelloTarget.wvc @@ -1,9 +1,10 @@ --- -import groowt.view.component.ComponentContext - -class Helper { - String greeting +void consume(out) { + out << 'World' } + +@groovy.transform.Field +String greeting = 'Hello' --- -Hello, $target! +$greeting, ${consume(out)}! What a nice day. diff --git a/web-views/src/main/java/groowt/view/web/ast/extension/GStringScriptletExtension.java b/web-views/src/main/java/groowt/view/web/ast/extension/GStringScriptletExtension.java index 6192b36..6dadf67 100644 --- a/web-views/src/main/java/groowt/view/web/ast/extension/GStringScriptletExtension.java +++ b/web-views/src/main/java/groowt/view/web/ast/extension/GStringScriptletExtension.java @@ -13,9 +13,4 @@ public non-sealed class GStringScriptletExtension extends GStringNodeExtension { super(self, allTokens.getRange(rawTokenRange)); } - @Override - public String getAsValidEmbeddableCode() { - return "${" + super.getAsValidEmbeddableCode() + "}"; - } - -} \ No newline at end of file +} diff --git a/web-views/src/main/java/groowt/view/web/ast/node/PreambleNode.java b/web-views/src/main/java/groowt/view/web/ast/node/PreambleNode.java index 9bc9f91..5731dfe 100644 --- a/web-views/src/main/java/groowt/view/web/ast/node/PreambleNode.java +++ b/web-views/src/main/java/groowt/view/web/ast/node/PreambleNode.java @@ -1,26 +1,46 @@ package groowt.view.web.ast.node; import groowt.util.di.annotation.Given; +import groowt.view.web.antlr.TokenList; +import groowt.view.web.ast.extension.GroovyCodeNodeExtension; import groowt.view.web.ast.extension.NodeExtensionContainer; import groowt.view.web.util.TokenRange; import jakarta.inject.Inject; +import org.antlr.v4.runtime.Token; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; public class PreambleNode extends AbstractLeafNode { - private final int groovyIndex; + private final GroovyCodeNodeExtension groovyCode; @Inject public PreambleNode( + TokenList tokenList, NodeExtensionContainer extensionContainer, @Given TokenRange tokenRange, @Given int groovyCodeIndex ) { super(tokenRange, extensionContainer); - this.groovyIndex = groovyCodeIndex; + this.groovyCode = this.createGroovyCode(tokenList, groovyCodeIndex); } - public int getGroovyCodeIndex() { - return this.groovyIndex; + protected GroovyCodeNodeExtension createGroovyCode(TokenList tokenList, int groovyCodeIndex) { + return this.createExtension( + GroovyCodeNodeExtension.class, + TokenRange.fromIndex(tokenList, groovyCodeIndex), + (Function, String>) this::toValidGroovyCode + ); + } + + protected String toValidGroovyCode(List tokenList) { + return tokenList.stream().map(Token::getText).collect(Collectors.joining()); + } + + public GroovyCodeNodeExtension getGroovyCode() { + return this.groovyCode; } } diff --git a/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentWriter.java b/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentWriter.java index 7fbf07b..52edc1d 100644 --- a/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentWriter.java +++ b/web-views/src/main/java/groowt/view/web/runtime/WebViewComponentWriter.java @@ -24,16 +24,12 @@ public class WebViewComponentWriter { } public void append(GString gString) { - final String content; try { - content = gString.toString(); - } catch (Exception exception) { - throw new ComponentRenderException(exception); - } - try { - this.delegate.append(content); + gString.writeTo(this.delegate); } catch (IOException ioException) { throw new RuntimeException(ioException); + } catch (Exception exception) { + throw new ComponentRenderException(exception); } } @@ -79,4 +75,13 @@ public class WebViewComponentWriter { } } + public void leftShift(Object object) { + switch (object) { + case String s -> this.append(s); + case GString gs -> this.append(gs); + case ViewComponent viewComponent -> this.append(viewComponent); + default -> this.append(object); + } + } + } diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultGStringTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultGStringTranspiler.java index e32ccf1..baed4d0 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/DefaultGStringTranspiler.java +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultGStringTranspiler.java @@ -8,6 +8,7 @@ import groowt.view.web.ast.extension.GStringScriptletExtension; import groowt.view.web.ast.node.GStringBodyTextNode; import groowt.view.web.ast.node.JStringBodyTextNode; import groowt.view.web.ast.node.Node; +import groowt.view.web.transpile.util.GroovyUtil; import groowt.view.web.util.FilteringIterable; import groowt.view.web.util.Option; import groowt.view.web.util.TokenRange; @@ -15,6 +16,9 @@ import jakarta.inject.Inject; import jakarta.inject.Singleton; import org.antlr.v4.runtime.Token; import org.codehaus.groovy.ast.expr.*; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.ExpressionStatement; +import org.codehaus.groovy.ast.stmt.Statement; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; @@ -70,7 +74,8 @@ public class DefaultGStringTranspiler implements GStringTranspiler { } protected record PathResult( - Expression result, Option before, + Expression result, + Option before, Option after ) {} @@ -124,6 +129,23 @@ public class DefaultGStringTranspiler implements GStringTranspiler { } } + protected ClosureExpression handleScriptlet(GStringScriptletExtension gStringScriptletExtension) { + final GroovyUtil.ConvertResult convertResult = GroovyUtil.convert( + "def cl = {" + gStringScriptletExtension.getAsValidEmbeddableCode() + "}" + ); + final BlockStatement convertBlock = convertResult.blockStatement(); + if (convertBlock == null) { + throw new NullPointerException("Did not except convertBlock to be null"); + } + final List convertStatements = convertBlock.getStatements(); + if (convertStatements.size() != 1) { + throw new IllegalStateException("Did not expect convertStatements.size() to not equal 1"); + } + final ExpressionStatement convertExpressionStatement = (ExpressionStatement) convertStatements.getFirst(); + final BinaryExpression assignment = (BinaryExpression) convertExpressionStatement.getExpression(); + return (ClosureExpression) assignment.getRightExpression(); + } + @Override public GStringExpression createGStringExpression(GStringBodyTextNode gStringBodyTextNode) { final var children = gStringBodyTextNode.getChildren(); @@ -136,7 +158,10 @@ public class DefaultGStringTranspiler implements GStringTranspiler { return jStringBodyTextNode.getContent(); } else if (node.hasExtension(GStringNodeExtension.class)) { final var gString = node.getExtension(GStringNodeExtension.class); - return gString.getAsValidEmbeddableCode(); + return switch (gString) { + case GStringPathExtension ignored -> gString.getAsValidEmbeddableCode(); + case GStringScriptletExtension ignored -> "${" + gString.getAsValidEmbeddableCode() + "}"; + }; } else { throw new IllegalArgumentException( "Cannot get verbatim text when one of the given parts has " @@ -165,7 +190,7 @@ public class DefaultGStringTranspiler implements GStringTranspiler { } case GStringScriptletExtension scriptlet -> { checkPrevBeforeDollar(prev, current).ifPresent(texts::add); - // TODO + values.add(this.handleScriptlet(scriptlet)); checkNextAfterDollar(current, next).ifPresent(texts::add); } } diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultGroovyTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultGroovyTranspiler.java index 79729cd..6088847 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/DefaultGroovyTranspiler.java +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultGroovyTranspiler.java @@ -3,10 +3,13 @@ package groowt.view.web.transpile; import groowt.view.web.antlr.TokenList; import groowt.view.web.ast.node.BodyNode; import groowt.view.web.ast.node.CompilationUnitNode; -import groowt.view.web.transpile.PreambleTranspiler.PreambleResult; +import groowt.view.web.ast.node.PreambleNode; +import groowt.view.web.transpile.util.GroovyUtil; import org.codehaus.groovy.ast.*; import org.codehaus.groovy.ast.expr.ClosureExpression; +import org.codehaus.groovy.ast.expr.DeclarationExpression; import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.ExpressionStatement; import org.codehaus.groovy.ast.stmt.ReturnStatement; import org.codehaus.groovy.ast.stmt.Statement; import org.codehaus.groovy.control.CompilationUnit; @@ -14,8 +17,11 @@ import org.codehaus.groovy.control.SourceUnit; import org.codehaus.groovy.control.io.ReaderSource; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.ArrayList; +import java.util.List; import java.util.function.Supplier; import static groowt.view.web.transpile.TranspilerUtil.*; @@ -30,6 +36,8 @@ import static org.objectweb.asm.Opcodes.ACC_PUBLIC; */ public class DefaultGroovyTranspiler implements GroovyTranspiler { + private static final Logger logger = LoggerFactory.getLogger(DefaultGroovyTranspiler.class); + private final CompilationUnit groovyCompilationUnit; private final String defaultPackageName; private final Supplier configurationSupplier; @@ -60,6 +68,96 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler { } } + protected void checkPreambleClasses(String templateName, List classNodes) { + final ClassNode offending = classNodes.stream() + .filter(classNode -> classNode.getName().equals(templateName)) + .findAny() + .orElse(null); + if (offending != null) { + throw new IllegalArgumentException( + templateName + " cannot define itself in the template. " + + "Remove the class with that name." + ); + } + } + + protected List convertPreambleClassesToInnerClasses(ClassNode mainClassNode, List classNodes) { + final List result = new ArrayList<>(); + for (final var classNode : classNodes) { + if (classNode instanceof InnerClassNode innerClassNode) { + result.add(innerClassNode); + } else { + final InnerClassNode icn = new InnerClassNode( + mainClassNode, + mainClassNode.getName() + "." + classNode.getNameWithoutPackage(), + classNode.getModifiers(), + classNode.getSuperClass(), + classNode.getInterfaces(), + classNode.getMixins() + ); + icn.setDeclaringClass(mainClassNode); + } + } + return result; + } + + protected void handlePreamble( + String templateName, + String packageName, + PreambleNode preambleNode, + ClassNode mainClassNode, + WebViewComponentModuleNode moduleNode + ) { + final GroovyUtil.ConvertResult preambleConvert = GroovyUtil.convert( + preambleNode.getGroovyCode().getAsValidGroovyCode() + ); + + WebViewComponentModuleNode.copyTo(preambleConvert.moduleNode(), moduleNode); + + final BlockStatement preambleBlock = preambleConvert.blockStatement(); + if (preambleBlock != null) { + // Fields + final List preambleStatements = preambleBlock.getStatements(); + final List declarationsWithField = preambleStatements.stream() + .filter(statement -> statement instanceof ExpressionStatement) + .map(ExpressionStatement.class::cast) + .map(ExpressionStatement::getExpression) + .filter(expression -> expression instanceof DeclarationExpression) + .map(DeclarationExpression.class::cast) + .filter(declarationExpression -> + !declarationExpression.getAnnotations(FIELD_ANNOTATION).isEmpty() + ) + .toList(); + if (declarationsWithField.size() != preambleStatements.size()) { + logger.warn( + "{} contains script statements which are not supported. " + + "Currently, only classes, methods, and field declarations (marked with @Field) " + + "are supported. The rest will be ignored.", + templateName + ); + } + declarationsWithField.forEach(declaration -> { + declaration.setDeclaringClass(mainClassNode); + }); + } + + // move methods from script class + final ClassNode scriptClass = preambleConvert.scriptClass(); + if (scriptClass != null) { + scriptClass.getMethods().forEach(mainClassNode::addMethod); + } + + // handle classes + final List classNodes = preambleConvert.classNodes(); + this.checkPreambleClasses(templateName, classNodes); + final List toInner = classNodes.stream() + .filter(classNode -> classNode != preambleConvert.scriptClass()) + .filter(classNode -> !classNode.isScript()) + .toList(); + final List innerClassNodes = this.convertPreambleClassesToInnerClasses(mainClassNode, toInner); + innerClassNodes.forEach(moduleNode::addClass); + } + // Cases: // - no preamble -> create our own class // - some preamble, but no script -> create our own class but use imports/packageName from preamble @@ -85,34 +183,26 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler { final var moduleNode = new WebViewComponentModuleNode(sourceUnit); sourceUnit.setModuleNode(moduleNode); - ClassNode mainClassNode; + final String packageName = this.getPackageName(moduleNode); + moduleNode.setPackageName(packageName); - final PreambleResult preambleResult = configuration.getPreambleTranspiler().getPreambleResult( - compilationUnitNode.getPreambleNode(), - templateName, - tokens + final ClassNode mainClassNode = new ClassNode( + packageName + "." + templateName, + ACC_PUBLIC, + ClassHelper.OBJECT_TYPE ); - if (preambleResult.moduleNode() != null) { - WebViewComponentModuleNode.copyTo(preambleResult.moduleNode(), moduleNode); - } - if (preambleResult.scriptClass() != null) { - mainClassNode = preambleResult.scriptClass(); - // do not add it to moduleNode because it's already there - } else { - final String packageName = this.getPackageName(moduleNode); - final String templateClassName = packageName + "." + templateName; + mainClassNode.setScript(true); + mainClassNode.addInterface(TranspilerUtil.COMPONENT_TEMPLATE); - mainClassNode = new ClassNode( - templateClassName, - ACC_PUBLIC, - ClassHelper.OBJECT_TYPE - ); - mainClassNode.setScript(true); - mainClassNode.addInterface(TranspilerUtil.COMPONENT_TEMPLATE); + moduleNode.addClass(mainClassNode); - moduleNode.addClass(mainClassNode); + // preamble + final PreambleNode preambleNode = compilationUnitNode.getPreambleNode(); + if (preambleNode != null) { + this.handlePreamble(templateName, packageName, preambleNode, mainClassNode, moduleNode); } + // renderer final var renderBlock = new BlockStatement(); final TranspilerState state = TranspilerState.withDefaultRootScope(); @@ -138,6 +228,8 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler { }, renderBlock ); + + // getRenderer() final Statement returnRendererStmt = new ReturnStatement(renderer); final var voidClosure = ClassHelper.CLOSURE_TYPE.getPlainNodeReference(); diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultPreambleTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultPreambleTranspiler.java deleted file mode 100644 index 7719be0..0000000 --- a/web-views/src/main/java/groowt/view/web/transpile/DefaultPreambleTranspiler.java +++ /dev/null @@ -1,32 +0,0 @@ -package groowt.view.web.transpile; - -import groowt.view.web.antlr.TokenList; -import groowt.view.web.ast.node.PreambleNode; -import groowt.view.web.transpile.util.GroovyUtil; -import org.antlr.v4.runtime.Token; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -public class DefaultPreambleTranspiler implements PreambleTranspiler { - - @Override - public PreambleResult getPreambleResult( - @Nullable PreambleNode preambleNode, - String templateName, - TokenList tokens - ) { - if (preambleNode == null) { - return new PreambleResult(null, null, List.of()); - } else { - final Token groovyToken = tokens.getGroovyToken(preambleNode.getGroovyCodeIndex()); - final GroovyUtil.ConvertResult convertResult = GroovyUtil.convert(groovyToken.getText(), templateName); - return new PreambleResult( - convertResult.moduleNode(), - convertResult.scriptClass(), - convertResult.classNodes() - ); - } - } - -} diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultTranspilerConfiguration.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultTranspilerConfiguration.java index 9327e8c..591ddee 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/DefaultTranspilerConfiguration.java +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultTranspilerConfiguration.java @@ -5,7 +5,6 @@ import jakarta.inject.Inject; public class DefaultTranspilerConfiguration implements TranspilerConfiguration { private final OutStatementFactory outStatementFactory = new SimpleOutStatementFactory(); - private final PreambleTranspiler preambleTranspiler = new DefaultPreambleTranspiler(); private final BodyTranspiler bodyTranspiler; @Inject @@ -20,11 +19,6 @@ public class DefaultTranspilerConfiguration implements TranspilerConfiguration { componentTranspiler.setValueNodeTranspiler(valueNodeTranspiler); } - @Override - public PreambleTranspiler getPreambleTranspiler() { - return this.preambleTranspiler; - } - @Override public BodyTranspiler getBodyTranspiler() { return this.bodyTranspiler; diff --git a/web-views/src/main/java/groowt/view/web/transpile/PreambleTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/PreambleTranspiler.java deleted file mode 100644 index 267e507..0000000 --- a/web-views/src/main/java/groowt/view/web/transpile/PreambleTranspiler.java +++ /dev/null @@ -1,20 +0,0 @@ -package groowt.view.web.transpile; - -import groowt.view.web.antlr.TokenList; -import groowt.view.web.ast.node.PreambleNode; -import org.codehaus.groovy.ast.ClassNode; -import org.codehaus.groovy.ast.ModuleNode; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -public interface PreambleTranspiler { - - record PreambleResult( - @Nullable ModuleNode moduleNode, @Nullable ClassNode scriptClass, - List allClasses - ) {} - - PreambleResult getPreambleResult(@Nullable PreambleNode preambleNode, String templateName, TokenList tokens); - -} diff --git a/web-views/src/main/java/groowt/view/web/transpile/TranspilerConfiguration.java b/web-views/src/main/java/groowt/view/web/transpile/TranspilerConfiguration.java index 1306cf8..a14f120 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/TranspilerConfiguration.java +++ b/web-views/src/main/java/groowt/view/web/transpile/TranspilerConfiguration.java @@ -1,7 +1,6 @@ package groowt.view.web.transpile; public interface TranspilerConfiguration { - PreambleTranspiler getPreambleTranspiler(); BodyTranspiler getBodyTranspiler(); OutStatementFactory getOutStatementFactory(); } diff --git a/web-views/src/main/java/groowt/view/web/transpile/TranspilerUtil.java b/web-views/src/main/java/groowt/view/web/transpile/TranspilerUtil.java index d155298..ec530f5 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/TranspilerUtil.java +++ b/web-views/src/main/java/groowt/view/web/transpile/TranspilerUtil.java @@ -1,6 +1,7 @@ package groowt.view.web.transpile; import groovy.lang.Tuple2; +import groovy.transform.Field; import groowt.view.component.ComponentContext; import groowt.view.component.ComponentTemplate; import groowt.view.web.runtime.WebViewComponentWriter; @@ -18,6 +19,7 @@ public final class TranspilerUtil { public static final ClassNode COMPONENT_TEMPLATE = ClassHelper.make(ComponentTemplate.class); public static final ClassNode OUT_TYPE = ClassHelper.make(WebViewComponentWriter.class); public static final ClassNode CONTEXT_CLASSNODE = ClassHelper.make(ComponentContext.class); + public static final ClassNode FIELD_ANNOTATION = ClassHelper.make(Field.class); public static final String GROOWT_VIEW_WEB = "groowt.view.web"; public static final String OUT = "out"; diff --git a/web-views/src/main/java/groowt/view/web/transpile/WebViewComponentModuleNode.java b/web-views/src/main/java/groowt/view/web/transpile/WebViewComponentModuleNode.java index a8a46fb..fe807b1 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/WebViewComponentModuleNode.java +++ b/web-views/src/main/java/groowt/view/web/transpile/WebViewComponentModuleNode.java @@ -14,16 +14,10 @@ import java.util.stream.Collectors; public class WebViewComponentModuleNode extends ModuleNode { public static void copyTo(ModuleNode from, WebViewComponentModuleNode to) { - to.setDescription(from.getDescription()); - to.setPackage(from.getPackage()); - to.setPackageName(from.getPackageName()); - to.setImportsResolved(from.hasImportsResolved()); - to.setMetaDataMap(from.getMetaDataMap()); from.getImports().forEach(to::addImport); from.getStarImports().forEach(to::addStarImport); from.getStaticImports().forEach(to::addStaticImport); from.getStaticStarImports().forEach(to::addStaticStarImport); - from.getClasses().forEach(to::addClass); } protected final List imports = new ArrayList<>(); diff --git a/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultPreambleTranspilerTests.java b/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultPreambleTranspilerTests.java deleted file mode 100644 index 1dae285..0000000 --- a/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultPreambleTranspilerTests.java +++ /dev/null @@ -1,15 +0,0 @@ -package groowt.view.web.transpiler; - -import groowt.view.web.transpile.DefaultPreambleTranspiler; - -public class DefaultPreambleTranspilerTests extends PreambleTranspilerTests { - - protected static DefaultPreambleTranspiler getDefaultPreambleTranspiler() { - return new DefaultPreambleTranspiler(); - } - - public DefaultPreambleTranspilerTests() { - super(getDefaultPreambleTranspiler()); - } - -} diff --git a/web-views/src/testFixtures/java/groowt/view/web/transpiler/GStringTranspilerTests.java b/web-views/src/testFixtures/java/groowt/view/web/transpiler/GStringTranspilerTests.java index ac3d870..5683912 100644 --- a/web-views/src/testFixtures/java/groowt/view/web/transpiler/GStringTranspilerTests.java +++ b/web-views/src/testFixtures/java/groowt/view/web/transpiler/GStringTranspilerTests.java @@ -4,12 +4,18 @@ import groowt.view.web.antlr.ParserUtil; import groowt.view.web.antlr.TokenList; import groowt.view.web.ast.DefaultAstBuilder; import groowt.view.web.ast.DefaultNodeFactory; +import groowt.view.web.ast.node.BodyNode; import groowt.view.web.ast.node.CompilationUnitNode; import groowt.view.web.ast.node.GStringBodyTextNode; import groowt.view.web.transpile.GStringTranspiler; +import org.codehaus.groovy.ast.expr.ClosureExpression; import org.codehaus.groovy.ast.expr.GStringExpression; +import org.codehaus.groovy.ast.expr.VariableExpression; import org.junit.jupiter.api.Test; +import java.util.Objects; +import java.util.function.Consumer; + import static org.junit.jupiter.api.Assertions.*; public abstract class GStringTranspilerTests { @@ -23,20 +29,55 @@ public abstract class GStringTranspilerTests { }); } - @Test - public void gStringExpressionWithDollarReference() { - final var source = "Hello, $target!"; + protected BodyNode getBodyNode(String source) { final var parseResult = ParserUtil.parseCompilationUnit(source); final var tokenList = new TokenList(parseResult.getTokenStream()); final var nodeFactory = new DefaultNodeFactory(tokenList); final var astBuilder = new DefaultAstBuilder(nodeFactory); final var cuNode = (CompilationUnitNode) astBuilder.build(parseResult.getCompilationUnitContext()); - final var bodyNode = cuNode.getBodyNode(); - assertNotNull(bodyNode); - final var gStringBodyTextNode = bodyNode.getAt(0, GStringBodyTextNode.class); + return Objects.requireNonNull(cuNode.getBodyNode()); + } + + protected void doTest(String source, Consumer further) { + final var gStringBodyTextNode = this.getBodyNode(source).getAt(0, GStringBodyTextNode.class); final var transpiler = this.getGStringTranspiler(); final GStringExpression gStringExpression = transpiler.createGStringExpression(gStringBodyTextNode); - assertEquals("Hello, $target!", gStringExpression.getText()); + assertEquals(source, gStringExpression.getText()); + further.accept(gStringExpression); + } + + @Test + public void gStringExpressionWithDollarReference() { + this.doTest("Hello, $target!", gStringExpression -> { + assertEquals(2, gStringExpression.getStrings().size()); + assertEquals(1, gStringExpression.getValues().size()); + }); + } + + @Test + public void multiplePathValues() { + this.doTest("$greeting, $target!", gStringExpression -> { + assertEquals(3, gStringExpression.getStrings().size()); + assertEquals(2, gStringExpression.getValues().size()); + final var firstValue = gStringExpression.getValue(0); + assertInstanceOf(VariableExpression.class, firstValue); + assertEquals("greeting", firstValue.getText()); + final var secondValue = gStringExpression.getValue(1); + assertInstanceOf(VariableExpression.class, secondValue); + assertEquals("target", secondValue.getText()); + }); + } + + @Test + public void pathAndClosure() { + this.doTest("$greeting, ${consume(out)}!", gStringExpression -> { + assertEquals(3, gStringExpression.getStrings().size()); + assertEquals(2, gStringExpression.getValues().size()); + final var firstValue = gStringExpression.getValue(0); + assertInstanceOf(VariableExpression.class, firstValue); + assertEquals("greeting", firstValue.getText()); + assertInstanceOf(ClosureExpression.class, gStringExpression.getValue(1)); + }); } } diff --git a/web-views/src/testFixtures/java/groowt/view/web/transpiler/PreambleTranspilerTests.java b/web-views/src/testFixtures/java/groowt/view/web/transpiler/PreambleTranspilerTests.java deleted file mode 100644 index a0271b8..0000000 --- a/web-views/src/testFixtures/java/groowt/view/web/transpiler/PreambleTranspilerTests.java +++ /dev/null @@ -1,17 +0,0 @@ -package groowt.view.web.transpiler; - -import groowt.view.web.transpile.PreambleTranspiler; -import org.junit.jupiter.api.Test; - -public abstract class PreambleTranspilerTests { - - protected final PreambleTranspiler preambleTranspiler; - - public PreambleTranspilerTests(PreambleTranspiler preambleTranspiler) { - this.preambleTranspiler = preambleTranspiler; - } - - @Test - public void smokeScreen() {} - -} diff --git a/web-views/src/tools/binTemplate.gst b/web-views/src/tools/binTemplate.gst index 9c878c3..316501a 100644 --- a/web-views/src/tools/binTemplate.gst +++ b/web-views/src/tools/binTemplate.gst @@ -1,3 +1,3 @@ #/usr/bin/env bash -java -cp build/libs/web-tools.jar:build/libs/web.jar $mainClassName "\$@" +../gradlew toolsJar && java -cp build/libs/web-tools.jar:build/libs/web.jar $mainClassName "\$@" diff --git a/web-views/src/tools/groovy/groowt/view/web/tools/ConvertToGroovy.groovy b/web-views/src/tools/groovy/groowt/view/web/tools/ConvertToGroovy.groovy index 2f900d4..20e9c8b 100644 --- a/web-views/src/tools/groovy/groowt/view/web/tools/ConvertToGroovy.groovy +++ b/web-views/src/tools/groovy/groowt/view/web/tools/ConvertToGroovy.groovy @@ -36,6 +36,9 @@ class ConvertToGroovy implements Callable { @CommandLine.Option(names = ['-o', '--out'], description = 'Write source files to disk instead of printing them.') boolean writeOut + @CommandLine.Option(names = ['-q', '--quiet'], description = 'Do not print the class source to the console.') + boolean quiet + // Default is Phases.CLASS_GENERATION (7) @CommandLine.Option( names = ['-t', '--compilePhase'], @@ -44,6 +47,12 @@ class ConvertToGroovy implements Callable { ) int compilePhase + @CommandLine.Option( + names = ['-c', '--classes'], + description = 'Whether to output the class files, if the compile phase is late enough. If this is false, any generated classes will be thrown out.' + ) + boolean doClasses + @CommandLine.Option( names = ['-d', '--classesDir'], description = 'If the GroovyCompiler outputs classes, where to write them.' @@ -60,7 +69,9 @@ class ConvertToGroovy implements Callable { def astBuilder = new DefaultAstBuilder(new DefaultNodeFactory(tokenList)) def cuNode = astBuilder.build(parseResult.compilationUnitContext) as CompilationUnitNode def config = new CompilerConfiguration().tap { - it.targetDirectory = this.classesDir ?: new File(target.parentFile, 'classes') + it.targetDirectory = this.doClasses + ? this.classesDir ?: new File(target.parentFile, 'classes') + : File.createTempDir() } def gcu = new CompilationUnit(config) @@ -84,7 +95,8 @@ class ConvertToGroovy implements Callable { if (this.writeOut) { def outFile = new File(target.parentFile, name + '.groovy') outFile.write(w.toString()) - } else { + } + if (!this.quiet) { println w.toString() } return true diff --git a/web-views/src/tools/groovy/groowt/view/web/tools/RunTemplate.groovy b/web-views/src/tools/groovy/groowt/view/web/tools/RunTemplate.groovy new file mode 100644 index 0000000..646d1fa --- /dev/null +++ b/web-views/src/tools/groovy/groowt/view/web/tools/RunTemplate.groovy @@ -0,0 +1,48 @@ +package groowt.view.web.tools + +import groowt.view.component.DefaultComponentContext +import groowt.view.web.DefaultWebComponentTemplateCompiler +import groowt.view.web.DefaultWebViewComponent +import groowt.view.web.WebViewTemplateComponentSource +import groowt.view.web.runtime.WebViewComponentWriter +import org.codehaus.groovy.control.CompilerConfiguration +import picocli.CommandLine +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import picocli.CommandLine.Parameters + +import java.util.concurrent.Callable + +@Command( + name = 'runTemplate', + description = 'render a wvc template with the given params' +) +class RunTemplate implements Callable { + + @Parameters(arity = '1', description = 'The template file.') + File template + + @Option( + names = ['-A', '--attr', '--attribute'], + description = 'Attribute(s) to pass to the template.' + ) + Map properties + + @Override + Integer call() throws Exception { + def component = new DefaultWebViewComponent(WebViewTemplateComponentSource.of(this.template)) + + def context = new DefaultComponentContext() + context.pushDefaultScope() + component.context = context + + println component.render() + + return 0 + } + + static void main(String[] args) { + System.exit(new CommandLine(new RunTemplate()).execute(args)) + } + +}