From ea4c29f1d784aec626d6445a924184297d787f60 Mon Sep 17 00:00:00 2001 From: JesseBrault0709 <62299747+JesseBrault0709@users.noreply.github.com> Date: Sat, 4 May 2024 12:55:42 +0200 Subject: [PATCH] Fragments and child rendering are working. --- .../component/AbstractComponentFactory.java | 72 +++---- .../view/component/AbstractViewComponent.java | 6 +- .../component/ClosureComponentFactory.java | 19 ++ .../view/component/ComponentContext.java | 14 +- .../view/component/ComponentFactory.java | 10 +- .../view/component/ComponentFactoryUtil.java | 53 +++++ .../component/DefaultComponentContext.java | 43 +++- .../component/DelegatingComponentFactory.java | 20 -- .../component/SupplierComponentFactory.java | 17 ++ .../groowt/view/component/ViewComponent.java | 3 + web-views/sketching/twoGreeters.wvc | 3 + .../src/main/antlr/WebViewComponentsParser.g4 | 2 +- .../groowt/view/web/ChildRenderException.java | 0 .../view/web/DefaultWebViewComponent.java | 23 ++- .../web/DefaultWebViewComponentContext.groovy | 26 +++ .../groowt/view/web/WebViewComponent.java | 24 +++ .../groowt/view/web/WebViewScope.groovy | 16 ++ .../web/lib/DelegatingWebViewComponent.java | 29 +++ .../groovy/groowt/view/web/lib/Echo.groovy | 82 ++++++++ .../groowt/view/web/lib/Fragment.groovy | 14 ++ .../DefaultWebComponentTemplateCompiler.java | 14 +- .../groowt/view/web/WebViewComponent.java | 11 - .../web/ast/DefaultAstBuilderVisitor.java | 2 +- .../view/web/ast/DefaultNodeFactory.java | 2 +- .../java/groowt/view/web/ast/NodeFactory.java | 2 +- .../web/ast/node/FragmentComponentNode.java | 13 +- .../view/web/lib/FragmentComponent.java | 8 - .../AppendOrAddStatementFactory.java | 20 ++ .../view/web/transpile/BodyTranspiler.java | 11 +- .../web/transpile/ComponentTranspiler.java | 3 - .../DefaultAppendOrAddStatementFactory.java | 85 ++++++++ .../web/transpile/DefaultBodyTranspiler.java | 19 +- .../transpile/DefaultComponentTranspiler.java | 190 ++++++++++++------ .../transpile/DefaultGStringTranspiler.java | 4 - .../transpile/DefaultGroovyTranspiler.java | 13 +- .../DefaultTranspilerConfiguration.java | 13 +- .../web/transpile/OutStatementFactory.java | 8 - .../transpile/SimpleOutStatementFactory.java | 27 --- .../transpile/TranspilerConfiguration.java | 2 +- .../view/web/transpile/TranspilerUtil.java | 19 ++ .../groowt/view/web/lib/EchoTemplate.gst | 1 + .../groowt/view/web/lib/FragmentTests.groovy | 47 +++++ .../DefaultBodyTranspilerTests.java | 17 +- .../groowt/view/web/ast/NodeFactoryTests.java | 5 - .../view/web/lib/AbstractComponentTests.java | 41 ++++ .../web/transpiler/BodyTranspilerTests.java | 21 +- 46 files changed, 793 insertions(+), 281 deletions(-) create mode 100644 view-components/src/main/java/groowt/view/component/ClosureComponentFactory.java create mode 100644 view-components/src/main/java/groowt/view/component/ComponentFactoryUtil.java delete mode 100644 view-components/src/main/java/groowt/view/component/DelegatingComponentFactory.java create mode 100644 view-components/src/main/java/groowt/view/component/SupplierComponentFactory.java create mode 100644 web-views/sketching/twoGreeters.wvc rename web-views/src/main/{java => groovy}/groowt/view/web/ChildRenderException.java (100%) rename web-views/src/main/{java => groovy}/groowt/view/web/DefaultWebViewComponent.java (82%) create mode 100644 web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponentContext.groovy create mode 100644 web-views/src/main/groovy/groowt/view/web/WebViewComponent.java create mode 100644 web-views/src/main/groovy/groowt/view/web/WebViewScope.groovy create mode 100644 web-views/src/main/groovy/groowt/view/web/lib/DelegatingWebViewComponent.java create mode 100644 web-views/src/main/groovy/groowt/view/web/lib/Echo.groovy create mode 100644 web-views/src/main/groovy/groowt/view/web/lib/Fragment.groovy delete mode 100644 web-views/src/main/java/groowt/view/web/WebViewComponent.java delete mode 100644 web-views/src/main/java/groowt/view/web/lib/FragmentComponent.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/AppendOrAddStatementFactory.java create mode 100644 web-views/src/main/java/groowt/view/web/transpile/DefaultAppendOrAddStatementFactory.java delete mode 100644 web-views/src/main/java/groowt/view/web/transpile/OutStatementFactory.java delete mode 100644 web-views/src/main/java/groowt/view/web/transpile/SimpleOutStatementFactory.java create mode 100644 web-views/src/main/resources/groowt/view/web/lib/EchoTemplate.gst create mode 100644 web-views/src/test/groovy/groowt/view/web/lib/FragmentTests.groovy create mode 100644 web-views/src/testFixtures/java/groowt/view/web/lib/AbstractComponentTests.java diff --git a/view-components/src/main/java/groowt/view/component/AbstractComponentFactory.java b/view-components/src/main/java/groowt/view/component/AbstractComponentFactory.java index 2733d4b..5abc9ce 100644 --- a/view-components/src/main/java/groowt/view/component/AbstractComponentFactory.java +++ b/view-components/src/main/java/groowt/view/component/AbstractComponentFactory.java @@ -9,50 +9,38 @@ import java.util.*; public abstract class AbstractComponentFactory extends GroovyObjectSupport implements ComponentFactory { - private static final String DO_CREATE = "doCreate"; - private static final Class[] EMPTY_CLASSES = new Class[0]; + protected final Map[], MetaMethod> cache = new HashMap<>(); - private static Object[] flatten(Object... args) { - if (args.length == 0) { - return args; - } else { - final List result = new ArrayList<>(args.length); - for (final var arg : args) { - if (arg instanceof Object[] arr) { - result.addAll(Arrays.asList(arr)); - } else { - result.add(arg); - } - } - return result.toArray(Object[]::new); - } - } - - private static Class[] asTypes(Object[] args) { - if (args.length == 0) { - return EMPTY_CLASSES; - } - final Class[] result = new Class[args.length]; - for (int i = 0; i < args.length; i++) { - result[i] = args[i].getClass(); - } - return result; - } - - private final Map[], MetaMethod> cache = new HashMap<>(); - - private MetaMethod findDoCreateMethod(Object[] allArgs) { - return this.cache.computeIfAbsent(asTypes(allArgs), types -> - this.getMetaClass().getMetaMethod(DO_CREATE, types) + protected MetaMethod findDoCreateMethod(Object[] allArgs) { + return this.cache.computeIfAbsent(ComponentFactoryUtil.asTypes(allArgs), types -> + ComponentFactoryUtil.findDoCreateMethod(this.getMetaClass(), types) ); } @SuppressWarnings("unchecked") - private T findAndDoCreate(ComponentContext componentContext, Object[] args) { - final Object[] contextsAndArgs = flatten(componentContext, args); - final MetaMethod contextsAndArgsMethod = this.findDoCreateMethod(contextsAndArgs); - if (contextsAndArgsMethod != null) { - return (T) contextsAndArgsMethod.invoke(this, contextsAndArgs); + protected T findAndDoCreate(Object type, ComponentContext componentContext, Object[] args) { + final Object[] typeContextAndArgs = ComponentFactoryUtil.flatten(type, componentContext, args); + final MetaMethod typeContextAndArgsMethod = this.findDoCreateMethod(typeContextAndArgs); + if (typeContextAndArgsMethod != null) { + return (T) typeContextAndArgsMethod.invoke(this, typeContextAndArgs); + } + + final Object[] typeAndContext = new Object[] { type, componentContext }; + final MetaMethod typeAndContextMethod = this.findDoCreateMethod(typeAndContext); + if (typeAndContextMethod != null) { + return (T) typeAndContextMethod.invoke(this, typeAndContext); + } + + final Object[] typeAndArgs = ComponentFactoryUtil.flatten(type, args); + final MetaMethod typeAndArgsMethod = this.findDoCreateMethod(typeAndArgs); + if (typeAndArgsMethod != null) { + return (T) typeAndArgsMethod.invoke(this, typeAndArgs); + } + + final Object[] typeOnly = new Object[] { type }; + final MetaMethod typeOnlyMethod = this.findDoCreateMethod(typeOnly); + if (typeOnlyMethod != null) { + return (T) typeOnlyMethod.invoke(this, typeOnly); } final Object[] contextOnly = new Object[] { componentContext }; @@ -67,15 +55,15 @@ public abstract class AbstractComponentFactory extends } throw new MissingMethodException( - DO_CREATE, + ComponentFactoryUtil.DO_CREATE, this.getClass(), args ); } @Override - public T create(ComponentContext componentContext, Object... args) { - return this.findAndDoCreate(componentContext, args); + public T create(Object type, ComponentContext componentContext, Object... args) { + return this.findAndDoCreate(type, componentContext, args); } } diff --git a/view-components/src/main/java/groowt/view/component/AbstractViewComponent.java b/view-components/src/main/java/groowt/view/component/AbstractViewComponent.java index 6f32170..2cc7012 100644 --- a/view-components/src/main/java/groowt/view/component/AbstractViewComponent.java +++ b/view-components/src/main/java/groowt/view/component/AbstractViewComponent.java @@ -32,10 +32,12 @@ public abstract class AbstractViewComponent implements ViewComponent { this.template = Objects.requireNonNull(template); } - protected void beforeRender() {} + protected void beforeRender() { + this.getContext().beforeComponentRender(this); + } protected void afterRender() { - this.getContext().afterComponent(this); + this.getContext().afterComponentRender(this); } @Override diff --git a/view-components/src/main/java/groowt/view/component/ClosureComponentFactory.java b/view-components/src/main/java/groowt/view/component/ClosureComponentFactory.java new file mode 100644 index 0000000..537e399 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/ClosureComponentFactory.java @@ -0,0 +1,19 @@ +package groowt.view.component; + +import groovy.lang.Closure; + +final class ClosureComponentFactory extends AbstractComponentFactory { + + private final Closure closure; + + public ClosureComponentFactory(Closure closure) { + this.closure = closure; + } + + @Override + public T create(Object type, ComponentContext componentContext, Object... args) { + final Object[] flattened = ComponentFactoryUtil.flatten(type, componentContext, args); + return this.closure.call(flattened); + } + +} diff --git a/view-components/src/main/java/groowt/view/component/ComponentContext.java b/view-components/src/main/java/groowt/view/component/ComponentContext.java index d997eba..477e255 100644 --- a/view-components/src/main/java/groowt/view/component/ComponentContext.java +++ b/view-components/src/main/java/groowt/view/component/ComponentContext.java @@ -11,13 +11,21 @@ import java.util.function.Predicate; public interface ComponentContext { @ApiStatus.Internal - ComponentFactory resolve(String component); + interface Resolved { + String getTypeName(); + ComponentFactory getComponentFactory(); + } @ApiStatus.Internal - ViewComponent create(ComponentFactory factory, Object... args); + Resolved resolve(String component); @ApiStatus.Internal - void afterComponent(ViewComponent component); + ViewComponent create(Resolved resolved, Object... args); + + void beforeComponentRender(ViewComponent component); + + @ApiStatus.Internal + void afterComponentRender(ViewComponent component); Deque getScopeStack(); diff --git a/view-components/src/main/java/groowt/view/component/ComponentFactory.java b/view-components/src/main/java/groowt/view/component/ComponentFactory.java index eecf080..21080ad 100644 --- a/view-components/src/main/java/groowt/view/component/ComponentFactory.java +++ b/view-components/src/main/java/groowt/view/component/ComponentFactory.java @@ -7,14 +7,14 @@ import java.util.function.Supplier; public interface ComponentFactory extends GroovyObject { - static ComponentFactory of(Closure closure) { - return new DelegatingComponentFactory<>((context, args) -> closure.call(context, args)); + static ComponentFactory ofClosure(Closure closure) { + return new ClosureComponentFactory<>(closure); } - static ComponentFactory of(Supplier supplier) { - return new DelegatingComponentFactory<>((ignored0, ignored1) -> supplier.get()); + static ComponentFactory ofSupplier(Supplier supplier) { + return new SupplierComponentFactory<>(supplier); } - T create(ComponentContext componentContext, Object... args); + T create(Object type, ComponentContext componentContext, Object... args); } diff --git a/view-components/src/main/java/groowt/view/component/ComponentFactoryUtil.java b/view-components/src/main/java/groowt/view/component/ComponentFactoryUtil.java new file mode 100644 index 0000000..da504e7 --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/ComponentFactoryUtil.java @@ -0,0 +1,53 @@ +package groowt.view.component; + +import groovy.lang.MetaClass; +import groovy.lang.MetaMethod; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public final class ComponentFactoryUtil { + + public static final String DO_CREATE = "doCreate"; + public static final Class[] EMPTY_CLASSES = new Class[0]; + + public static Object[] flatten(Object... args) { + if (args.length == 0) { + return args; + } else { + final List result = new ArrayList<>(args.length); + for (final var arg : args) { + if (arg instanceof Object[] arr) { + result.addAll(Arrays.asList(arr)); + } else { + result.add(arg); + } + } + return result.toArray(Object[]::new); + } + } + + public static Class[] asTypes(Object[] args) { + if (args.length == 0) { + return EMPTY_CLASSES; + } + final Class[] result = new Class[args.length]; + for (int i = 0; i < args.length; i++) { + final Object arg = args[i]; + if (arg instanceof Class argAsClass) { + result[i] = argAsClass; + } else { + result[i] = arg.getClass(); + } + } + return result; + } + + public static MetaMethod findDoCreateMethod(MetaClass metaClass, Class[] types) { + return metaClass.getMetaMethod(DO_CREATE, types); + } + + private ComponentFactoryUtil() {} + +} diff --git a/view-components/src/main/java/groowt/view/component/DefaultComponentContext.java b/view-components/src/main/java/groowt/view/component/DefaultComponentContext.java index 7b9401d..a88853b 100644 --- a/view-components/src/main/java/groowt/view/component/DefaultComponentContext.java +++ b/view-components/src/main/java/groowt/view/component/DefaultComponentContext.java @@ -10,11 +10,33 @@ import java.util.function.Predicate; public class DefaultComponentContext implements ComponentContext { + protected static class DefaultResolved implements ComponentContext.Resolved { + + private final String typeName; + private final ComponentFactory factory; + + public DefaultResolved(String typeName, ComponentFactory factory) { + this.typeName = typeName; + this.factory = factory; + } + + @Override + public String getTypeName() { + return this.typeName; + } + + @Override + public ComponentFactory getComponentFactory() { + return this.factory; + } + + } + private final Deque scopeStack = new LinkedList<>(); private final Deque componentStack = new LinkedList<>(); @Override - public ComponentFactory resolve(String component) { + public Resolved resolve(String component) { if (scopeStack.isEmpty()) { throw new IllegalStateException("There are no scopes on the scopeStack."); } @@ -23,7 +45,7 @@ public class DefaultComponentContext implements ComponentContext { while (!getStack.isEmpty()) { final ComponentScope scope = getStack.pop(); if (scope.contains(component)) { - return scope.get(component); + return new DefaultResolved(component, scope.get(component)); } } @@ -32,7 +54,7 @@ public class DefaultComponentContext implements ComponentContext { while (!missingStack.isEmpty()) { final ComponentScope scope = getStack.pop(); try { - return scope.factoryMissing(component); + return new DefaultResolved(component, scope.factoryMissing(component)); } catch (NoFactoryMissingException e) { if (first == null) { first = e; @@ -48,14 +70,19 @@ public class DefaultComponentContext implements ComponentContext { } @Override - public ViewComponent create(ComponentFactory factory, Object... args) { - final ViewComponent component = factory.create(this, args); - this.componentStack.push(component); - return component; + public ViewComponent create(Resolved resolved, Object... args) { + return resolved.getComponentFactory().create( + resolved.getTypeName(), this, args + ); } @Override - public void afterComponent(ViewComponent component) { + public void beforeComponentRender(ViewComponent component) { + this.componentStack.push(component); + } + + @Override + public void afterComponentRender(ViewComponent component) { final var popped = this.componentStack.pop(); if (!popped.equals(component)) { throw new IllegalStateException("Popped component does not equal arg to afterComponent()"); diff --git a/view-components/src/main/java/groowt/view/component/DelegatingComponentFactory.java b/view-components/src/main/java/groowt/view/component/DelegatingComponentFactory.java deleted file mode 100644 index b0633a4..0000000 --- a/view-components/src/main/java/groowt/view/component/DelegatingComponentFactory.java +++ /dev/null @@ -1,20 +0,0 @@ -package groowt.view.component; - -final class DelegatingComponentFactory extends AbstractComponentFactory { - - @FunctionalInterface - interface ComponentFactoryDelegate { - T doCreate(ComponentContext context, Object... args); - } - - private final ComponentFactoryDelegate function; - - public DelegatingComponentFactory(ComponentFactoryDelegate function) { - this.function = function; - } - - public T doCreate(ComponentContext componentContext, Object... args) { - return this.function.doCreate(componentContext, args); - } - -} diff --git a/view-components/src/main/java/groowt/view/component/SupplierComponentFactory.java b/view-components/src/main/java/groowt/view/component/SupplierComponentFactory.java new file mode 100644 index 0000000..534c82c --- /dev/null +++ b/view-components/src/main/java/groowt/view/component/SupplierComponentFactory.java @@ -0,0 +1,17 @@ +package groowt.view.component; + +import java.util.function.Supplier; + +final class SupplierComponentFactory extends AbstractComponentFactory { + + private final Supplier tSupplier; + + public SupplierComponentFactory(Supplier tSupplier) { + this.tSupplier = tSupplier; + } + + public T doCreate(Object type, ComponentContext componentContext, Object... args) { + return this.tSupplier.get(); + } + +} diff --git a/view-components/src/main/java/groowt/view/component/ViewComponent.java b/view-components/src/main/java/groowt/view/component/ViewComponent.java index 0ed85a7..cfbd7a0 100644 --- a/view-components/src/main/java/groowt/view/component/ViewComponent.java +++ b/view-components/src/main/java/groowt/view/component/ViewComponent.java @@ -1,6 +1,7 @@ package groowt.view.component; import groowt.view.View; +import org.jetbrains.annotations.ApiStatus; public interface ViewComponent extends View { @@ -8,7 +9,9 @@ public interface ViewComponent extends View { return this.getClass().getName(); } + @ApiStatus.Internal void setContext(ComponentContext context); + ComponentContext getContext(); } diff --git a/web-views/sketching/twoGreeters.wvc b/web-views/sketching/twoGreeters.wvc new file mode 100644 index 0000000..555d949 --- /dev/null +++ b/web-views/sketching/twoGreeters.wvc @@ -0,0 +1,3 @@ +<> +   + diff --git a/web-views/src/main/antlr/WebViewComponentsParser.g4 b/web-views/src/main/antlr/WebViewComponentsParser.g4 index 69c7539..1442da3 100644 --- a/web-views/src/main/antlr/WebViewComponentsParser.g4 +++ b/web-views/src/main/antlr/WebViewComponentsParser.g4 @@ -56,7 +56,7 @@ closingComponent fragmentComponent : ComponentOpen ComponentClose - body? + body ClosingComponentOpen ComponentClose ; diff --git a/web-views/src/main/java/groowt/view/web/ChildRenderException.java b/web-views/src/main/groovy/groowt/view/web/ChildRenderException.java similarity index 100% rename from web-views/src/main/java/groowt/view/web/ChildRenderException.java rename to web-views/src/main/groovy/groowt/view/web/ChildRenderException.java diff --git a/web-views/src/main/java/groowt/view/web/DefaultWebViewComponent.java b/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponent.java similarity index 82% rename from web-views/src/main/java/groowt/view/web/DefaultWebViewComponent.java rename to web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponent.java index 71aae6c..c053340 100644 --- a/web-views/src/main/java/groowt/view/web/DefaultWebViewComponent.java +++ b/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponent.java @@ -2,13 +2,11 @@ package groowt.view.web; import groovy.lang.Closure; import groovy.lang.GroovyClassLoader; -import groowt.util.di.DefaultRegistryObjectFactory; import groowt.view.component.AbstractViewComponent; import groowt.view.component.ComponentContext; import groowt.view.component.ComponentTemplate; import groowt.view.web.WebViewTemplateComponentSource.*; import groowt.view.web.runtime.WebViewComponentWriter; -import groowt.view.web.transpile.DefaultTranspilerConfiguration; import org.codehaus.groovy.control.CompilerConfiguration; import org.jetbrains.annotations.Nullable; @@ -77,25 +75,36 @@ public class DefaultWebViewComponent extends AbstractViewComponent implements We } @Override - public List getChildren() { - return Objects.requireNonNullElseGet(this.children, ArrayList::new); + public List getChildRenderers() { + if (this.children == null) { + this.children = new ArrayList<>(); + } + return this.children; } @Override - public void setChildren(List children) { + public boolean hasChildren() { + return !this.getChildRenderers().isEmpty(); + } + + @Override + public void setChildRenderers(List children) { this.children = children; } @Override public void renderChildren() { - for (final var childRenderer : this.getChildren()) { + for (final var childRenderer : this.getChildRenderers()) { try { + if (childRenderer instanceof WebViewChildComponentRenderer childComponentRenderer) { + this.getContext().beforeComponentRender(childComponentRenderer.getComponent()); + } childRenderer.render(this); } catch (Exception e) { throw new ChildRenderException(e); } finally { if (childRenderer instanceof WebViewChildComponentRenderer childComponentRenderer) { - this.getContext().afterComponent(childComponentRenderer.getComponent()); + this.getContext().afterComponentRender(childComponentRenderer.getComponent()); } } } diff --git a/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponentContext.groovy b/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponentContext.groovy new file mode 100644 index 0000000..c1f3a74 --- /dev/null +++ b/web-views/src/main/groovy/groowt/view/web/DefaultWebViewComponentContext.groovy @@ -0,0 +1,26 @@ +package groowt.view.web + +import groowt.view.component.ComponentScope +import groowt.view.component.DefaultComponentContext +import groowt.view.component.ViewComponent +import groowt.view.web.lib.Fragment +import groowt.view.web.runtime.WebViewComponentChildCollector +import org.jetbrains.annotations.ApiStatus + +class DefaultWebViewComponentContext extends DefaultComponentContext { + + @Override + protected ComponentScope getNewDefaultScope() { + new WebViewScope() + } + + @ApiStatus.Internal + ViewComponent createFragment(Closure childCollector) { + def collector = new WebViewComponentChildCollector() + childCollector.call(collector) + def fragment = new Fragment() + fragment.childRenderers = collector.children + fragment + } + +} diff --git a/web-views/src/main/groovy/groowt/view/web/WebViewComponent.java b/web-views/src/main/groovy/groowt/view/web/WebViewComponent.java new file mode 100644 index 0000000..dcc98d4 --- /dev/null +++ b/web-views/src/main/groovy/groowt/view/web/WebViewComponent.java @@ -0,0 +1,24 @@ +package groowt.view.web; + +import groowt.view.component.ViewComponent; + +import java.util.List; + +public interface WebViewComponent extends ViewComponent { + + List getChildRenderers(); + boolean hasChildren(); + void setChildRenderers(List children); + void renderChildren(); + + default List getChildren() { + return this.getChildRenderers().stream() + .map(childRenderer -> switch (childRenderer) { + case WebViewChildComponentRenderer componentRenderer -> componentRenderer.getComponent(); + case WebViewChildGStringRenderer gStringRenderer -> gStringRenderer.getGString(); + case WebViewChildJStringRenderer jStringRenderer -> jStringRenderer.getContent(); + }) + .toList(); + } + +} diff --git a/web-views/src/main/groovy/groowt/view/web/WebViewScope.groovy b/web-views/src/main/groovy/groowt/view/web/WebViewScope.groovy new file mode 100644 index 0000000..4f5b981 --- /dev/null +++ b/web-views/src/main/groovy/groowt/view/web/WebViewScope.groovy @@ -0,0 +1,16 @@ +package groowt.view.web + +import groowt.view.component.ComponentFactory +import groowt.view.component.DefaultComponentScope +import groowt.view.web.lib.Echo.EchoFactory + +class WebViewScope extends DefaultComponentScope { + + private final EchoFactory echoFactory = new EchoFactory() + + @Override + ComponentFactory factoryMissing(String typeName) { + echoFactory + } + +} diff --git a/web-views/src/main/groovy/groowt/view/web/lib/DelegatingWebViewComponent.java b/web-views/src/main/groovy/groowt/view/web/lib/DelegatingWebViewComponent.java new file mode 100644 index 0000000..34a8a30 --- /dev/null +++ b/web-views/src/main/groovy/groowt/view/web/lib/DelegatingWebViewComponent.java @@ -0,0 +1,29 @@ +package groowt.view.web.lib; + +import groowt.view.View; +import groowt.view.web.DefaultWebViewComponent; + +import java.io.IOException; +import java.io.Writer; +import java.util.Map; + +public abstract class DelegatingWebViewComponent extends DefaultWebViewComponent { + + private final Map attr; + + public DelegatingWebViewComponent(Map attr) { + this.attr = attr; + } + + protected Map getAttr() { + return this.attr; + } + + protected abstract View getDelegate(); + + @Override + public void renderTo(Writer out) throws IOException { + this.getDelegate().renderTo(out); + } + +} diff --git a/web-views/src/main/groovy/groowt/view/web/lib/Echo.groovy b/web-views/src/main/groovy/groowt/view/web/lib/Echo.groovy new file mode 100644 index 0000000..9f4f34d --- /dev/null +++ b/web-views/src/main/groovy/groowt/view/web/lib/Echo.groovy @@ -0,0 +1,82 @@ +package groowt.view.web.lib + +import groowt.view.StandardGStringTemplateView +import groowt.view.View +import groowt.view.component.AbstractComponentFactory +import groowt.view.component.ComponentContext +import groowt.view.component.ComponentFactory +import groowt.view.web.WebViewChildComponentRenderer + +class Echo extends DelegatingWebViewComponent { + + static final class EchoFactory implements ComponentFactory { + + Echo doCreate(String typeName, ComponentContext context, Map attr) { + doCreate(typeName, context, attr, true) + } + + Echo doCreate(String typeName, ComponentContext context, Map attr, boolean selfClose) { + def echo = new Echo(attr, typeName, selfClose) + echo.context = context + echo + } + + Echo doCreate( + String typeName, + ComponentContext context, + Map attr, + List children + ) { + def echo = new Echo(attr, typeName, false) + echo.context = context + echo.childRenderers = children + echo + } + + @Override + Echo create(Object type, ComponentContext componentContext, Object... args) { + if (!type instanceof String) { + throw new IllegalArgumentException(' can only be used with String types.') + } + if (args == null || args.length < 1) { + throw new IllegalArgumentException( + ' must have at least one attribute. ' + + 'If you are just echoing children, use a fragment (<>...) instead. ' + ) + } + this.invokeMethod('doCreate', type as String, componentContext, *args) as Echo + } + + } + + String typeName + boolean selfClose + + Echo(Map attr, String typeName, boolean selfClose) { + super(attr) + this.typeName = typeName + } + + @Override + protected View getDelegate() { + return new StandardGStringTemplateView( + src: Echo.getResource('EchoTemplate.gst'), + parent: this + ) + } + + void renderAttr(Writer out) { + def iter = this.attr.iterator() + while (iter.hasNext()) { + def entry = iter.next() + out << entry.key + out << '="' + out << entry.value + out << '"' + if (iter.hasNext()) { + out << ' ' + } + } + } + +} diff --git a/web-views/src/main/groovy/groowt/view/web/lib/Fragment.groovy b/web-views/src/main/groovy/groowt/view/web/lib/Fragment.groovy new file mode 100644 index 0000000..c51924b --- /dev/null +++ b/web-views/src/main/groovy/groowt/view/web/lib/Fragment.groovy @@ -0,0 +1,14 @@ +package groowt.view.web.lib + +import groowt.view.web.DefaultWebViewComponent + +class Fragment extends DefaultWebViewComponent { + + @Override + void renderTo(Writer out) throws IOException { + this.beforeRender() + this.renderChildren() + this.afterRender() + } + +} diff --git a/web-views/src/main/java/groowt/view/web/DefaultWebComponentTemplateCompiler.java b/web-views/src/main/java/groowt/view/web/DefaultWebComponentTemplateCompiler.java index c53af22..9f6b948 100644 --- a/web-views/src/main/java/groowt/view/web/DefaultWebComponentTemplateCompiler.java +++ b/web-views/src/main/java/groowt/view/web/DefaultWebComponentTemplateCompiler.java @@ -1,7 +1,6 @@ package groowt.view.web; import groovy.lang.GroovyClassLoader; -import groowt.util.di.RegistryObjectFactory; import groowt.view.component.CachingComponentTemplateCompiler; import groowt.view.component.ComponentTemplate; import groowt.view.component.ComponentTemplateCreateException; @@ -14,9 +13,7 @@ import groowt.view.web.ast.DefaultNodeFactory; import groowt.view.web.ast.node.CompilationUnitNode; import groowt.view.web.transpile.DefaultGroovyTranspiler; import groowt.view.web.transpile.DefaultTranspilerConfiguration; -import groowt.view.web.transpile.TranspilerConfiguration; import org.codehaus.groovy.control.CompilationUnit; -import org.codehaus.groovy.control.CompilePhase; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.Phases; import org.codehaus.groovy.control.io.AbstractReaderSource; @@ -31,7 +28,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Objects; -import java.util.function.Supplier; public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplateCompiler { @@ -74,11 +70,11 @@ public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplat this.groovyClassLoader = null; } - protected ComponentTemplate doCompile(Class forClass, Reader reader) { + protected ComponentTemplate doCompile(@Nullable Class forClass, Reader reader) { return this.doCompile(forClass, reader, null); } - protected ComponentTemplate doCompile(Class forClass, Reader reader, @Nullable URI uri) { + protected ComponentTemplate doCompile(@Nullable Class forClass, Reader reader, @Nullable URI uri) { final CompilationUnitParseResult parseResult = ParserUtil.parseCompilationUnit(reader); // TODO: analysis @@ -94,7 +90,7 @@ public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplat DefaultTranspilerConfiguration::new ); - final var ownerComponentName = forClass.getSimpleName(); + final var ownerComponentName = forClass != null ? forClass.getSimpleName() : "AnonymousComponent"; final var templateClassName = ownerComponentName + "Template"; final var fqn = this.defaultPackageName + "." + templateClassName; @@ -205,4 +201,8 @@ public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplat return this.getFromCacheOrElse(forClass, () -> this.doCompile(forClass, reader)); } + public ComponentTemplate compileAnonymous(Reader reader) { + return this.doCompile(null, reader); + } + } diff --git a/web-views/src/main/java/groowt/view/web/WebViewComponent.java b/web-views/src/main/java/groowt/view/web/WebViewComponent.java deleted file mode 100644 index 0e3692a..0000000 --- a/web-views/src/main/java/groowt/view/web/WebViewComponent.java +++ /dev/null @@ -1,11 +0,0 @@ -package groowt.view.web; - -import groowt.view.component.ViewComponent; - -import java.util.List; - -public interface WebViewComponent extends ViewComponent { - List getChildren(); - void setChildren(List children); - void renderChildren(); -} diff --git a/web-views/src/main/java/groowt/view/web/ast/DefaultAstBuilderVisitor.java b/web-views/src/main/java/groowt/view/web/ast/DefaultAstBuilderVisitor.java index c0ba4e0..2462b24 100644 --- a/web-views/src/main/java/groowt/view/web/ast/DefaultAstBuilderVisitor.java +++ b/web-views/src/main/java/groowt/view/web/ast/DefaultAstBuilderVisitor.java @@ -189,7 +189,7 @@ public class DefaultAstBuilderVisitor extends WebViewComponentsParserBaseVisitor public Node visitFragmentComponent(WebViewComponentsParser.FragmentComponentContext ctx) { return this.nodeFactory.fragmentComponentNode( this.getTokenRange(ctx), - this.getSingleAs(ctx.body(), BodyNode.class) + this.getSingleAsNonNull(ctx.body(), BodyNode.class) ); } diff --git a/web-views/src/main/java/groowt/view/web/ast/DefaultNodeFactory.java b/web-views/src/main/java/groowt/view/web/ast/DefaultNodeFactory.java index 58cdb11..99d80b3 100644 --- a/web-views/src/main/java/groowt/view/web/ast/DefaultNodeFactory.java +++ b/web-views/src/main/java/groowt/view/web/ast/DefaultNodeFactory.java @@ -125,7 +125,7 @@ public class DefaultNodeFactory implements NodeFactory { } @Override - public FragmentComponentNode fragmentComponentNode(TokenRange tokenRange, @Nullable BodyNode bodyNode) { + public FragmentComponentNode fragmentComponentNode(TokenRange tokenRange, BodyNode bodyNode) { return this.objectFactory.get(FragmentComponentNode.class, tokenRange, bodyNode); } diff --git a/web-views/src/main/java/groowt/view/web/ast/NodeFactory.java b/web-views/src/main/java/groowt/view/web/ast/NodeFactory.java index cf64da2..8519ba6 100644 --- a/web-views/src/main/java/groowt/view/web/ast/NodeFactory.java +++ b/web-views/src/main/java/groowt/view/web/ast/NodeFactory.java @@ -28,7 +28,7 @@ public interface NodeFactory { @Nullable BodyNode body ); - FragmentComponentNode fragmentComponentNode(TokenRange tokenRange, @Nullable BodyNode bodyNode); + FragmentComponentNode fragmentComponentNode(TokenRange tokenRange, BodyNode bodyNode); ComponentArgsNode componentArgsNode( TokenRange tokenRange, diff --git a/web-views/src/main/java/groowt/view/web/ast/node/FragmentComponentNode.java b/web-views/src/main/java/groowt/view/web/ast/node/FragmentComponentNode.java index fc2a364..9010804 100644 --- a/web-views/src/main/java/groowt/view/web/ast/node/FragmentComponentNode.java +++ b/web-views/src/main/java/groowt/view/web/ast/node/FragmentComponentNode.java @@ -4,7 +4,9 @@ import groowt.util.di.annotation.Given; import groowt.view.web.ast.extension.NodeExtensionContainer; import groowt.view.web.util.TokenRange; import jakarta.inject.Inject; -import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Objects; public non-sealed class FragmentComponentNode extends ComponentNode { @@ -12,9 +14,14 @@ public non-sealed class FragmentComponentNode extends ComponentNode { public FragmentComponentNode( NodeExtensionContainer extensionContainer, @Given TokenRange tokenRange, - @Given @Nullable BodyNode body + @Given BodyNode body ) { - super(tokenRange, extensionContainer, filterNulls(body), body); + super(tokenRange, extensionContainer, List.of(Objects.requireNonNull(body)), body); + } + + @Override + public BodyNode getBody() { + return Objects.requireNonNull(super.getBody()); } } diff --git a/web-views/src/main/java/groowt/view/web/lib/FragmentComponent.java b/web-views/src/main/java/groowt/view/web/lib/FragmentComponent.java deleted file mode 100644 index c68927b..0000000 --- a/web-views/src/main/java/groowt/view/web/lib/FragmentComponent.java +++ /dev/null @@ -1,8 +0,0 @@ -package groowt.view.web.lib; - -import groowt.view.web.DefaultWebViewComponent; - -// TODO: anything special? -public class FragmentComponent extends DefaultWebViewComponent { - -} diff --git a/web-views/src/main/java/groowt/view/web/transpile/AppendOrAddStatementFactory.java b/web-views/src/main/java/groowt/view/web/transpile/AppendOrAddStatementFactory.java new file mode 100644 index 0000000..c80fcde --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/AppendOrAddStatementFactory.java @@ -0,0 +1,20 @@ +package groowt.view.web.transpile; + +import groowt.view.web.ast.node.BodyChildNode; +import groowt.view.web.transpile.TranspilerUtil.TranspilerState; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.stmt.Statement; + +import java.util.function.Function; + +public interface AppendOrAddStatementFactory { + + enum Action { + ADD, APPEND + } + + Statement addOnly(BodyChildNode sourceNode, TranspilerState state, Expression rightSide); + Statement appendOnly(BodyChildNode sourceNode, TranspilerState state, Expression rightSide); + Statement addOrAppend(BodyChildNode sourceNode, TranspilerState state, Function getRightSide); + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/BodyTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/BodyTranspiler.java index 3710dda..a188fa0 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/BodyTranspiler.java +++ b/web-views/src/main/java/groowt/view/web/transpile/BodyTranspiler.java @@ -1,25 +1,22 @@ package groowt.view.web.transpile; +import groowt.view.web.ast.node.BodyChildNode; import groowt.view.web.ast.node.BodyNode; -import groowt.view.web.ast.node.Node; import groowt.view.web.transpile.TranspilerUtil.TranspilerState; -import org.codehaus.groovy.ast.VariableScope; import org.codehaus.groovy.ast.expr.Expression; import org.codehaus.groovy.ast.stmt.BlockStatement; import org.codehaus.groovy.ast.stmt.Statement; -import java.util.List; - public interface BodyTranspiler { @FunctionalInterface - interface ExpressionStatementConverter { - Statement createStatement(Node source, Expression expression); + interface AddOrAppendCallback { + Statement createStatement(BodyChildNode source, Expression expression); } BlockStatement transpileBody( BodyNode bodyNode, - ExpressionStatementConverter converter, + AddOrAppendCallback addOrAppendCallback, TranspilerState state ); diff --git a/web-views/src/main/java/groowt/view/web/transpile/ComponentTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/ComponentTranspiler.java index ebb2397..667aeab 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/ComponentTranspiler.java +++ b/web-views/src/main/java/groowt/view/web/transpile/ComponentTranspiler.java @@ -2,10 +2,7 @@ package groowt.view.web.transpile; import groowt.view.web.ast.node.ComponentNode; import groowt.view.web.transpile.TranspilerUtil.TranspilerState; -import org.codehaus.groovy.ast.Variable; -import org.codehaus.groovy.ast.expr.VariableExpression; import org.codehaus.groovy.ast.stmt.BlockStatement; -import org.codehaus.groovy.ast.stmt.Statement; public interface ComponentTranspiler { BlockStatement createComponentStatements( diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultAppendOrAddStatementFactory.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultAppendOrAddStatementFactory.java new file mode 100644 index 0000000..b2d97f9 --- /dev/null +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultAppendOrAddStatementFactory.java @@ -0,0 +1,85 @@ +package groowt.view.web.transpile; + +import groovy.lang.Tuple2; +import groowt.view.web.ast.NodeUtil; +import groowt.view.web.ast.node.BodyChildNode; +import groowt.view.web.ast.node.ComponentNode; +import groowt.view.web.ast.node.GStringBodyTextNode; +import groowt.view.web.transpile.TranspilerUtil.TranspilerState; +import org.codehaus.groovy.ast.expr.*; +import org.codehaus.groovy.ast.stmt.ExpressionStatement; +import org.codehaus.groovy.ast.stmt.Statement; + +import java.util.function.Function; + +public class DefaultAppendOrAddStatementFactory implements AppendOrAddStatementFactory { + + private void addLineAndColumn( + BodyChildNode bodyChildNode, + TupleExpression args + ) { + final Tuple2 lineAndColumn = TranspilerUtil.lineAndColumn( + bodyChildNode.asNode().getTokenRange().getStartPosition() + ); + args.addExpression(lineAndColumn.getV1()); + args.addExpression(lineAndColumn.getV2()); + } + + private Statement doCreate( + BodyChildNode bodyChildNode, + Expression rightSide, + VariableExpression target, + String methodName, + boolean addLineAndColumn + ) { + final ArgumentListExpression args; + if (rightSide instanceof ArgumentListExpression argumentListExpression) { + args = argumentListExpression; + } else { + args = new ArgumentListExpression(); + args.addExpression(rightSide); + } + if (addLineAndColumn && + NodeUtil.isAnyOfType(bodyChildNode.asNode(), GStringBodyTextNode.class, ComponentNode.class)) { + this.addLineAndColumn(bodyChildNode, args); + } + final MethodCallExpression outExpression = new MethodCallExpression(target, methodName, args); + return new ExpressionStatement(outExpression); + } + + @Override + public Statement addOnly(BodyChildNode bodyChildNode, TranspilerState state, Expression rightSide) { + return this.doCreate( + bodyChildNode, + rightSide, + new VariableExpression(state.getCurrentChildCollector()), + TranspilerUtil.ADD, + false + ); + } + + @Override + public Statement appendOnly(BodyChildNode bodyChildNode, TranspilerState state, Expression rightSide) { + return this.doCreate( + bodyChildNode, + rightSide, + new VariableExpression(state.out()), + TranspilerUtil.APPEND, + true + ); + } + + @Override + public Statement addOrAppend( + BodyChildNode bodyChildNode, + TranspilerState state, + Function getRightSide + ) { + if (state.hasCurrentChildCollector()) { + return this.addOnly(bodyChildNode, state, getRightSide.apply(Action.ADD)); + } else { + return this.appendOnly(bodyChildNode, state, getRightSide.apply(Action.APPEND)); + } + } + +} diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultBodyTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultBodyTranspiler.java index d68db22..f371331 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/DefaultBodyTranspiler.java +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultBodyTranspiler.java @@ -3,15 +3,9 @@ package groowt.view.web.transpile; import groowt.view.web.ast.node.*; import groowt.view.web.transpile.TranspilerUtil.TranspilerState; import jakarta.inject.Inject; -import jakarta.inject.Singleton; import org.codehaus.groovy.ast.expr.GStringExpression; import org.codehaus.groovy.ast.stmt.BlockStatement; -import org.codehaus.groovy.ast.stmt.Statement; -import java.util.ArrayList; -import java.util.List; - -@Singleton public class DefaultBodyTranspiler implements BodyTranspiler { private final GStringTranspiler gStringTranspiler; @@ -32,7 +26,7 @@ public class DefaultBodyTranspiler implements BodyTranspiler { @Override public BlockStatement transpileBody( BodyNode bodyNode, - ExpressionStatementConverter converter, + AddOrAppendCallback addOrAppendCallback, TranspilerState state ) { final BlockStatement block = new BlockStatement(); @@ -43,22 +37,19 @@ public class DefaultBodyTranspiler implements BodyTranspiler { final GStringExpression gString = this.gStringTranspiler.createGStringExpression( gStringBodyTextNode ); - block.addStatement(converter.createStatement(gStringBodyTextNode, gString)); + block.addStatement(addOrAppendCallback.createStatement(gStringBodyTextNode, gString)); } case JStringBodyTextNode jStringBodyTextNode -> { block.addStatement( - converter.createStatement( + addOrAppendCallback.createStatement( jStringBodyTextNode, this.jStringTranspiler.createStringLiteral(jStringBodyTextNode) ) ); } case ComponentNode componentNode -> { - final BlockStatement componentBlock = this.componentTranspiler.createComponentStatements( - componentNode, - state - ); - block.addStatement(componentBlock); + // DO NOT add/append this, because the component transpiler does it already + block.addStatement(this.componentTranspiler.createComponentStatements(componentNode, state)); } case PlainScriptletNode plainScriptletNode -> { throw new UnsupportedOperationException("TODO"); diff --git a/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java b/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java index ab91044..f7eccc0 100644 --- a/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java +++ b/web-views/src/main/java/groowt/view/web/transpile/DefaultComponentTranspiler.java @@ -1,10 +1,9 @@ package groowt.view.web.transpile; +import groovy.lang.Tuple2; import groowt.view.component.*; import groowt.view.web.ast.node.*; -import groowt.view.web.lib.FragmentComponent; import groowt.view.web.runtime.WebViewComponentChildCollector; -import groowt.view.web.transpile.TranspilerUtil.TranspilerState; import groowt.view.web.transpile.util.GroovyUtil; import groowt.view.web.transpile.util.GroovyUtil.ConvertResult; import org.codehaus.groovy.ast.*; @@ -16,9 +15,9 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; +import java.util.Objects; -import static groowt.view.web.transpile.TranspilerUtil.lineAndColumn; -import static groowt.view.web.transpile.TranspilerUtil.makeStringLiteral; +import static groowt.view.web.transpile.TranspilerUtil.*; public class DefaultComponentTranspiler implements ComponentTranspiler { @@ -33,16 +32,19 @@ public class DefaultComponentTranspiler implements ComponentTranspiler { private static final ClassNode MISSING_COMPONENT_EXCEPTION = ClassHelper.make(MissingComponentException.class); private static final ClassNode MISSING_CLASS_TYPE_EXCEPTION = ClassHelper.make(MissingClassTypeException.class); private static final ClassNode MISSING_STRING_TYPE_EXCEPTION = ClassHelper.make(MissingStringTypeException.class); - private static final ClassNode MISSING_FRAGMENT_TYPE_EXCEPTION = ClassHelper.make(MissingFragmentTypeException.class); + private static final ClassNode MISSING_FRAGMENT_TYPE_EXCEPTION = + ClassHelper.make(MissingFragmentTypeException.class); private static final String CREATE = "create"; + private static final String CREATE_FRAGMENT = "createFragment"; private static final String RESOLVE = "resolve"; private static final String ADD = "add"; private static final String APPEND = "append"; - private static final String FRAGMENT_FQN = FragmentComponent.class.getCanonicalName(); + private static final String FRAGMENT_FQN = GROOWT_VIEW_WEB + ".lib.Fragment"; private ValueNodeTranspiler valueNodeTranspiler; private BodyTranspiler bodyTranspiler; + private AppendOrAddStatementFactory appendOrAddStatementFactory; public void setValueNodeTranspiler(ValueNodeTranspiler valueNodeTranspiler) { this.valueNodeTranspiler = valueNodeTranspiler; @@ -52,6 +54,10 @@ public class DefaultComponentTranspiler implements ComponentTranspiler { this.bodyTranspiler = bodyTranspiler; } + public void setAppendOrAddStatementFactory(AppendOrAddStatementFactory appendOrAddStatementFactory) { + this.appendOrAddStatementFactory = Objects.requireNonNull(appendOrAddStatementFactory); + } + // ViewComponent c0 protected ExpressionStatement getComponentDeclaration(Variable component) { final var componentDeclaration = new DeclarationExpression( @@ -82,9 +88,7 @@ public class DefaultComponentTranspiler implements ComponentTranspiler { } // key: value - protected MapEntryExpression getAttrExpression( - AttrNode attrNode, TranspilerState state - ) { + protected MapEntryExpression getAttrExpression(AttrNode attrNode, TranspilerState state) { final var keyExpr = makeStringLiteral(attrNode.getKeyNode().getKey()); final Expression valueExpr = switch (attrNode) { case BooleanValueAttrNode ignored -> ConstantExpression.PRIM_TRUE; @@ -129,13 +133,13 @@ public class DefaultComponentTranspiler implements ComponentTranspiler { args.addExpression(lineAndColumn.getV2()); } - protected MethodCallExpression getOutCall(Node sourceNode, TranspilerState state, Expression toOutput) { + protected MethodCallExpression getOutCall(BodyChildNode sourceNode, TranspilerState state, Expression toOutput) { final VariableExpression outVariableExpr = new VariableExpression(state.out()); final ArgumentListExpression args = new ArgumentListExpression(); args.addExpression(toOutput); switch (sourceNode) { - case GStringBodyTextNode ignored -> this.addLineAndColumn(sourceNode, args); - case ComponentNode ignored -> this.addLineAndColumn(sourceNode, args); + case GStringBodyTextNode ignored -> this.addLineAndColumn(sourceNode.asNode(), args); + case ComponentNode ignored -> this.addLineAndColumn(sourceNode.asNode(), args); default -> { } } @@ -143,7 +147,7 @@ public class DefaultComponentTranspiler implements ComponentTranspiler { } // { out << jString | gString | component } - protected ClosureExpression getOutClosure(Node sourceNode, TranspilerState state, Expression toRender) { + protected ClosureExpression getOutClosure(BodyChildNode sourceNode, TranspilerState state, Expression toRender) { if (toRender instanceof VariableExpression variableExpression) { variableExpression.setClosureSharedVariable(true); } @@ -153,7 +157,7 @@ public class DefaultComponentTranspiler implements ComponentTranspiler { // c0_childCollector.add (jString | gString | component) { out << ... } protected Statement getChildCollectorAdd( - Node sourceNode, + BodyChildNode sourceNode, TranspilerState state, Variable childCollector, Expression toAdd @@ -168,8 +172,15 @@ public class DefaultComponentTranspiler implements ComponentTranspiler { return new ExpressionStatement(methodCall); } + /** + * @return Tuple containing 1. body ClosureExpression, and 2. childCollector Variable + */ // { WebViewComponentChildCollector c0_childCollector -> ... } - protected ClosureExpression getBodyClosure(BodyNode bodyNode, TranspilerState state, String componentVariableName) { + protected Tuple2 getBodyClosure( + BodyNode bodyNode, + TranspilerState state, + String componentVariableName + ) { final Parameter childCollectorParam = new Parameter( CHILD_COLLECTOR, componentVariableName + "_childCollector" @@ -177,26 +188,40 @@ public class DefaultComponentTranspiler implements ComponentTranspiler { final var scope = state.pushScope(); scope.putDeclaredVariable(childCollectorParam); + state.pushChildCollector(childCollectorParam); final BlockStatement bodyStatements = this.bodyTranspiler.transpileBody( bodyNode, (sourceNode, expr) -> this.getChildCollectorAdd(sourceNode, state, childCollectorParam, expr), state ); + state.popChildCollector(); state.popScope(); - return new ClosureExpression(new Parameter[]{childCollectorParam}, bodyStatements); + final ClosureExpression bodyClosure = new ClosureExpression( + new Parameter[] { childCollectorParam }, + bodyStatements + ); + return new Tuple2<>(bodyClosure, childCollectorParam); } + /** + * @return Tuple containing 1. create Expression, + * and 2. childCollector Variable, possibly {@code null}. + */ // context.create(...) {...} - protected MethodCallExpression getCreateExpression( - ComponentNode componentNode, TranspilerState state, String componentVariableName + protected Tuple2 getCreateExpression( + ComponentNode componentNode, + TranspilerState state, + String componentVariableName ) { final var createArgs = new ArgumentListExpression(); - - final var contextResolve = this.getContextResolveExpr(componentNode, state.context()); - createArgs.addExpression(contextResolve); - + final String createName; + Variable childCollector = null; if (componentNode instanceof TypedComponentNode typedComponentNode) { + createName = CREATE; + final var contextResolve = this.getContextResolveExpr(componentNode, state.context()); + createArgs.addExpression(contextResolve); + final List attributeNodes = typedComponentNode.getArgs().getAttributes(); if (!attributeNodes.isEmpty()) { createArgs.addExpression(this.getAttrMap(attributeNodes, state)); @@ -205,35 +230,62 @@ public class DefaultComponentTranspiler implements ComponentTranspiler { if (constructorNode != null) { this.getConstructorArgs(constructorNode).forEach(createArgs::addExpression); } + + final @Nullable BodyNode bodyNode = componentNode.getBody(); + if (bodyNode != null) { + final var bodyResult = this.getBodyClosure(bodyNode, state, componentVariableName); + childCollector = bodyResult.getV2(); + createArgs.addExpression(bodyResult.getV1()); + } + } else if (componentNode instanceof FragmentComponentNode fragmentComponentNode) { + createName = CREATE_FRAGMENT; + final BodyNode bodyNode = Objects.requireNonNull( + fragmentComponentNode.getBody(), + "FragmentComponentNode cannot have a null body." + ); + final var bodyResult = this.getBodyClosure(bodyNode, state, componentVariableName); + childCollector = bodyResult.getV2(); + createArgs.addExpression(bodyResult.getV1()); + } else { + throw new IllegalArgumentException("Unsupported ComponentNode type: " + componentNode.getClass().getName()); } - final @Nullable BodyNode bodyNode = componentNode.getBody(); - if (bodyNode != null) { - createArgs.addExpression(this.getBodyClosure(bodyNode, state, componentVariableName)); - } + final var createCall = new MethodCallExpression( + new VariableExpression(state.context()), + createName, + createArgs + ); - return new MethodCallExpression(new VariableExpression(state.context()), CREATE, createArgs); + return new Tuple2<>(createCall, childCollector); } + /** + * @return Tuple containing 1. assignment ExpressionStatement, + * and 2. childCollector Variable, possibly {@code null}. + */ // c0 = context.create(context.resolve(''), [:], ...) {...} - protected ExpressionStatement getCreateAssignStatement( - ComponentNode componentNode, TranspilerState state, String componentVariableName + protected Tuple2 getCreateAssignStatement( + ComponentNode componentNode, + TranspilerState state, + String componentVariableName, + Variable component ) { - final var componentAssignLeft = new VariableExpression(state.getDeclaredVariable(componentVariableName)); - final var createExpr = this.getCreateExpression(componentNode, state, componentVariableName); + final var componentAssignLeft = new VariableExpression(component); + final var createExprResult = this.getCreateExpression(componentNode, state, componentVariableName); final var componentAssignExpr = new BinaryExpression( componentAssignLeft, new Token(Types.ASSIGN, "=", -1, -1), - createExpr + createExprResult.getV1() ); - return new ExpressionStatement(componentAssignExpr); + return new Tuple2<>(new ExpressionStatement(componentAssignExpr), createExprResult.getV2()); } // catch (NoFactoryMissingException c0nfme) { // throw new MissingClassComponentException(this, 'ComponentType', c0nfme) // } protected CatchStatement getNoMissingFactoryExceptionCatch( - ComponentNode componentNode, String componentVariableName + ComponentNode componentNode, + String componentVariableName ) { final String exceptionName = componentVariableName + "nfme"; final Parameter fmeParam = new Parameter(NO_FACTORY_MISSING_EXCEPTION, exceptionName); @@ -281,7 +333,10 @@ public class DefaultComponentTranspiler implements ComponentTranspiler { } // catch (Exception c0ce) { throw new ComponentCreateException(c0ce) } - protected CatchStatement getGeneralCreateExceptionCatch(ComponentNode componentNode, String componentVariableName) { + protected CatchStatement getGeneralCreateExceptionCatch( + ComponentNode componentNode, + String componentVariableName + ) { final String exceptionName = componentVariableName + "ce"; final Parameter exceptionParam = new Parameter(EXCEPTION, exceptionName); final VariableExpression exceptionVar = new VariableExpression(exceptionName); @@ -317,53 +372,68 @@ public class DefaultComponentTranspiler implements ComponentTranspiler { return new ExpressionStatement(setContext); } - protected Statement createComponentOutCall( - ComponentNode componentNode, - TranspilerState state, - Variable component - ) { - // out << c0 - final VariableExpression toOutput = new VariableExpression(component); - final Expression outCallExpr = this.getOutCall(componentNode, state, toOutput); - return new ExpressionStatement(outCallExpr); - } - protected String getComponentVariableName(int componentNumber) { return "c" + componentNumber; } @Override - public BlockStatement createComponentStatements( - ComponentNode componentNode, - TranspilerState state - ) { + public BlockStatement createComponentStatements(ComponentNode componentNode, TranspilerState state) { final var componentVariableName = this.getComponentVariableName(state.newComponentNumber()); - final Variable component = new VariableExpression(componentVariableName, VIEW_COMPONENT); + final VariableExpression component = new VariableExpression(componentVariableName, VIEW_COMPONENT); final BlockStatement result = new BlockStatement(); - final VariableScope scope = state.pushScope(); + final VariableScope scope = state.currentScope(); result.setVariableScope(scope); scope.putDeclaredVariable(component); // ViewComponent c0; result.addStatement(this.getComponentDeclaration(component)); - // try { context.create(...) } catch { ... } - final var tryCreateStatement = new TryCatchStatement(this.getCreateAssignStatement( + // c0 = context.create(...) { ... } + final var createAssignStatementResult = this.getCreateAssignStatement( componentNode, state, - componentVariableName - ), EmptyStatement.INSTANCE); + componentVariableName, + component + ); + + // try { ... } catch { ... } + final var tryCreateStatement = new TryCatchStatement( + createAssignStatementResult.getV1(), + EmptyStatement.INSTANCE + ); this.getCreateCatches(componentNode, componentVariableName).forEach(tryCreateStatement::addCatch); result.addStatement(tryCreateStatement); // component.setContext(context) result.addStatement(this.createSetContext(state, component)); - // out << component - result.addStatement(this.createComponentOutCall(componentNode, state, component)); - - state.popScope(); + // out or collect + final var addOrAppend = this.appendOrAddStatementFactory.addOrAppend( + componentNode, + state, + action -> switch (action) { + case ADD -> { + final var args = new ArgumentListExpression(); + args.addExpression(component); + final var outComponent = new VariableExpression(component); + outComponent.setClosureSharedVariable(true); + final Statement renderStatement = this.appendOrAddStatementFactory.appendOnly( + componentNode, + state, + outComponent + ); + final ClosureExpression renderArg = new ClosureExpression( + Parameter.EMPTY_ARRAY, + renderStatement + ); + args.addExpression(renderArg); + yield args; + } + case APPEND -> new VariableExpression(component); + } + ); + result.addStatement(addOrAppend); return result; } 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 baed4d0..b7b2166 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 @@ -12,8 +12,6 @@ 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; -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; @@ -26,13 +24,11 @@ import java.util.List; import java.util.ListIterator; import java.util.stream.Collectors; -@Singleton public class DefaultGStringTranspiler implements GStringTranspiler { private final PositionSetter positionSetter; private final JStringTranspiler jStringTranspiler; - @Inject public DefaultGStringTranspiler(PositionSetter positionSetter, JStringTranspiler jStringTranspiler) { this.positionSetter = positionSetter; this.jStringTranspiler = jStringTranspiler; 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 7dab8d2..b9518da 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 @@ -210,12 +210,17 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler { final BodyNode bodyNode = compilationUnitNode.getBodyNode(); if (bodyNode != null) { - final var outStatementFactory = configuration.getOutStatementFactory(); + final var appendOrAddStatementFactory = configuration.getAppendOrAddStatementFactory(); renderBlock.addStatement( configuration.getBodyTranspiler() .transpileBody( compilationUnitNode.getBodyNode(), - (ignored, expr) -> outStatementFactory.create(expr), + (source, expr) -> appendOrAddStatementFactory.addOrAppend(source, state, action -> { + if (action == AppendOrAddStatementFactory.Action.ADD) { + throw new IllegalStateException("Should not be adding here!"); + } + return expr; + }), state ) ); @@ -223,8 +228,8 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler { final ClosureExpression renderer = new ClosureExpression( new Parameter[] { - (Parameter) state.getDeclaredVariable(CONTEXT), - (Parameter) state.getDeclaredVariable(OUT) + (Parameter) state.context(), + (Parameter) state.out() }, renderBlock ); 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 591ddee..d15d171 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 @@ -4,7 +4,7 @@ import jakarta.inject.Inject; public class DefaultTranspilerConfiguration implements TranspilerConfiguration { - private final OutStatementFactory outStatementFactory = new SimpleOutStatementFactory(); + private final AppendOrAddStatementFactory appendOrAddStatementFactory = new DefaultAppendOrAddStatementFactory(); private final BodyTranspiler bodyTranspiler; @Inject @@ -13,10 +13,13 @@ public class DefaultTranspilerConfiguration implements TranspilerConfiguration { final var jStringTranspiler = new DefaultJStringTranspiler(positionSetter); final var gStringTranspiler = new DefaultGStringTranspiler(positionSetter, jStringTranspiler); final var componentTranspiler = new DefaultComponentTranspiler(); - this.bodyTranspiler = new DefaultBodyTranspiler(gStringTranspiler, jStringTranspiler, componentTranspiler); - componentTranspiler.setBodyTranspiler(this.bodyTranspiler); final var valueNodeTranspiler = new DefaultValueNodeTranspiler(componentTranspiler); + + this.bodyTranspiler = new DefaultBodyTranspiler(gStringTranspiler, jStringTranspiler, componentTranspiler); + + componentTranspiler.setBodyTranspiler(this.bodyTranspiler); componentTranspiler.setValueNodeTranspiler(valueNodeTranspiler); + componentTranspiler.setAppendOrAddStatementFactory(this.appendOrAddStatementFactory); } @Override @@ -25,8 +28,8 @@ public class DefaultTranspilerConfiguration implements TranspilerConfiguration { } @Override - public OutStatementFactory getOutStatementFactory() { - return this.outStatementFactory; + public AppendOrAddStatementFactory getAppendOrAddStatementFactory() { + return this.appendOrAddStatementFactory; } } diff --git a/web-views/src/main/java/groowt/view/web/transpile/OutStatementFactory.java b/web-views/src/main/java/groowt/view/web/transpile/OutStatementFactory.java deleted file mode 100644 index 039dfbc..0000000 --- a/web-views/src/main/java/groowt/view/web/transpile/OutStatementFactory.java +++ /dev/null @@ -1,8 +0,0 @@ -package groowt.view.web.transpile; - -import org.codehaus.groovy.ast.expr.Expression; -import org.codehaus.groovy.ast.stmt.Statement; - -public interface OutStatementFactory { - Statement create(Expression rightSide); -} diff --git a/web-views/src/main/java/groowt/view/web/transpile/SimpleOutStatementFactory.java b/web-views/src/main/java/groowt/view/web/transpile/SimpleOutStatementFactory.java deleted file mode 100644 index 02bde05..0000000 --- a/web-views/src/main/java/groowt/view/web/transpile/SimpleOutStatementFactory.java +++ /dev/null @@ -1,27 +0,0 @@ -package groowt.view.web.transpile; - -import groowt.view.web.transpile.util.GroovyUtil; -import org.codehaus.groovy.ast.expr.BinaryExpression; -import org.codehaus.groovy.ast.expr.Expression; -import org.codehaus.groovy.ast.expr.MethodCallExpression; -import org.codehaus.groovy.ast.expr.VariableExpression; -import org.codehaus.groovy.ast.stmt.ExpressionStatement; -import org.codehaus.groovy.ast.stmt.Statement; -import org.codehaus.groovy.syntax.Types; - -import static org.codehaus.groovy.syntax.Token.newSymbol; - -public class SimpleOutStatementFactory implements OutStatementFactory { - - @Override - public Statement create(Expression rightSide) { - final VariableExpression out = new VariableExpression("out"); - final MethodCallExpression methodCallExpression = new MethodCallExpression( - out, - "append", - rightSide - ); - return new ExpressionStatement(methodCallExpression); - } - -} 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 a14f120..d16b4ad 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 @@ -2,5 +2,5 @@ package groowt.view.web.transpile; public interface TranspilerConfiguration { BodyTranspiler getBodyTranspiler(); - OutStatementFactory getOutStatementFactory(); + AppendOrAddStatementFactory getAppendOrAddStatementFactory(); } 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 ec530f5..19fbb42 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 @@ -25,6 +25,8 @@ public final class TranspilerUtil { public static final String OUT = "out"; public static final String CONTEXT = "context"; public static final String GET_RENDERER = "getRenderer"; + public static final String APPEND = "append"; + public static final String ADD = "add"; public static Tuple2 lineAndColumn(SourcePosition sourcePosition) { return new Tuple2<>( @@ -58,6 +60,7 @@ public final class TranspilerUtil { private final AtomicInteger componentCounter = new AtomicInteger(); private final Deque scopeStack = new LinkedList<>(); + private final Deque childCollectorStack = new LinkedList<>(); private TranspilerState(VariableScope rootScope) { this.scopeStack.push(rootScope); @@ -103,6 +106,22 @@ public final class TranspilerUtil { throw new NullPointerException("Cannot find variable: " + name); } + public void popChildCollector() { + this.childCollectorStack.pop(); + } + + public void pushChildCollector(Variable childCollector) { + this.childCollectorStack.push(childCollector); + } + + public Variable getCurrentChildCollector() { + return Objects.requireNonNull(this.childCollectorStack.peek()); + } + + public boolean hasCurrentChildCollector() { + return this.childCollectorStack.peek() != null; + } + } private TranspilerUtil() {} diff --git a/web-views/src/main/resources/groowt/view/web/lib/EchoTemplate.gst b/web-views/src/main/resources/groowt/view/web/lib/EchoTemplate.gst new file mode 100644 index 0000000..c028d2c --- /dev/null +++ b/web-views/src/main/resources/groowt/view/web/lib/EchoTemplate.gst @@ -0,0 +1 @@ +<$typeName ${renderAttr()}${selfClose ? ' /' : ''}>${renderChildren()}${!selfClose ? "" : ''} diff --git a/web-views/src/test/groovy/groowt/view/web/lib/FragmentTests.groovy b/web-views/src/test/groovy/groowt/view/web/lib/FragmentTests.groovy new file mode 100644 index 0000000..e84db8d --- /dev/null +++ b/web-views/src/test/groovy/groowt/view/web/lib/FragmentTests.groovy @@ -0,0 +1,47 @@ +package groowt.view.web.lib + +import groowt.view.component.ComponentContext +import groowt.view.component.ComponentFactory +import groowt.view.web.DefaultWebViewComponent +import groowt.view.web.DefaultWebViewComponentContext +import groowt.view.web.WebViewTemplateComponentSource +import org.junit.jupiter.api.Test + +class FragmentTests extends AbstractComponentTests { + + static class Greeter extends DefaultWebViewComponent { + + String greeting + + Greeter(Map attr) { + super(WebViewTemplateComponentSource.of('$greeting')) + greeting = attr.greeting + } + + } + + private final ComponentContext greeterContext = new DefaultWebViewComponentContext().tap { + pushDefaultScope() + def greeterFactory = ComponentFactory.ofClosure { type, componentContext, attr -> + new Greeter(attr) + } + currentScope.add('Greeter', greeterFactory) + } + + @Test + void simple() { + this.doTest('<>', 'Hello, World!', this.greeterContext) + } + + @Test + void multipleChildren() { + this.doTest( + ''' + <> +   + + '''.stripIndent(), 'Hello, one! Hello, two!', this.greeterContext + ) + } + +} diff --git a/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultBodyTranspilerTests.java b/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultBodyTranspilerTests.java index f1586d3..d820e1a 100644 --- a/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultBodyTranspilerTests.java +++ b/web-views/src/test/groovy/groowt/view/web/transpiler/DefaultBodyTranspilerTests.java @@ -5,21 +5,8 @@ import groowt.view.web.transpile.*; public class DefaultBodyTranspilerTests extends BodyTranspilerTests { @Override - protected BodyTranspiler getBodyTranspiler() { - final var positionSetter = new SimplePositionSetter(); - final var jStringTranspiler = new DefaultJStringTranspiler(positionSetter); - final var gStringTranspiler = new DefaultGStringTranspiler(positionSetter, jStringTranspiler); - final var componentTranspiler = new DefaultComponentTranspiler(); - final var valueNodeTranspiler = new DefaultValueNodeTranspiler(componentTranspiler); - componentTranspiler.setValueNodeTranspiler(valueNodeTranspiler); - final var bodyTranspiler = new DefaultBodyTranspiler(gStringTranspiler, jStringTranspiler, componentTranspiler); - componentTranspiler.setBodyTranspiler(bodyTranspiler); - return bodyTranspiler; - } - - @Override - protected OutStatementFactory getOutStatementFactory() { - return new SimpleOutStatementFactory(); + protected TranspilerConfiguration getConfiguration() { + return new DefaultTranspilerConfiguration(); } } diff --git a/web-views/src/testFixtures/java/groowt/view/web/ast/NodeFactoryTests.java b/web-views/src/testFixtures/java/groowt/view/web/ast/NodeFactoryTests.java index 219f16e..c208305 100644 --- a/web-views/src/testFixtures/java/groowt/view/web/ast/NodeFactoryTests.java +++ b/web-views/src/testFixtures/java/groowt/view/web/ast/NodeFactoryTests.java @@ -100,11 +100,6 @@ public abstract class NodeFactoryTests { assertNotNull(this.nodeFactory.fragmentComponentNode(this.getTokenRange(), bodyNode)); } - @Test - public void fragmentComponentNodeBodyNull() { - assertNotNull(this.nodeFactory.fragmentComponentNode(this.getTokenRange(), null)); - } - @Test public void componentArgsNodeWithClassComponentType( @Mock ClassComponentTypeNode componentTypeNode, diff --git a/web-views/src/testFixtures/java/groowt/view/web/lib/AbstractComponentTests.java b/web-views/src/testFixtures/java/groowt/view/web/lib/AbstractComponentTests.java new file mode 100644 index 0000000..7c2b304 --- /dev/null +++ b/web-views/src/testFixtures/java/groowt/view/web/lib/AbstractComponentTests.java @@ -0,0 +1,41 @@ +package groowt.view.web.lib; + +import groovy.lang.Closure; +import groowt.view.component.ComponentContext; +import groowt.view.component.ComponentTemplate; +import groowt.view.web.DefaultWebComponentTemplateCompiler; +import groowt.view.web.DefaultWebViewComponentContext; +import groowt.view.web.runtime.WebViewComponentWriter; +import org.codehaus.groovy.control.CompilerConfiguration; + +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; + +import static org.junit.jupiter.api.Assertions.*; + +public abstract class AbstractComponentTests { + + protected void doTest(Reader source, String expected, ComponentContext context) { + final var compiler = new DefaultWebComponentTemplateCompiler( + CompilerConfiguration.DEFAULT, this.getClass().getPackageName() + ); + final ComponentTemplate template = compiler.compileAnonymous(source); + final Closure renderer = template.getRenderer(); + final StringWriter sw = new StringWriter(); + final WebViewComponentWriter out = new WebViewComponentWriter(sw); + renderer.call(context, out); + assertEquals(expected, sw.toString()); + } + + protected void doTest(String source, String expected, ComponentContext context) { + this.doTest(new StringReader(source), expected, context); + } + + protected void doTest(String source, String expected) { + final var context = new DefaultWebViewComponentContext(); + context.pushDefaultScope(); + this.doTest(source, expected, context); + } + +} diff --git a/web-views/src/testFixtures/java/groowt/view/web/transpiler/BodyTranspilerTests.java b/web-views/src/testFixtures/java/groowt/view/web/transpiler/BodyTranspilerTests.java index 1abcf72..9ca8ce5 100644 --- a/web-views/src/testFixtures/java/groowt/view/web/transpiler/BodyTranspilerTests.java +++ b/web-views/src/testFixtures/java/groowt/view/web/transpiler/BodyTranspilerTests.java @@ -7,7 +7,7 @@ import groowt.view.web.ast.DefaultNodeFactory; import groowt.view.web.ast.node.BodyNode; import groowt.view.web.ast.node.CompilationUnitNode; import groowt.view.web.transpile.BodyTranspiler; -import groowt.view.web.transpile.OutStatementFactory; +import groowt.view.web.transpile.TranspilerConfiguration; import groowt.view.web.transpile.TranspilerUtil; import org.codehaus.groovy.ast.expr.ConstantExpression; import org.codehaus.groovy.ast.expr.MethodCallExpression; @@ -20,8 +20,7 @@ import static org.junit.jupiter.api.Assertions.*; public abstract class BodyTranspilerTests { - protected abstract BodyTranspiler getBodyTranspiler(); - protected abstract OutStatementFactory getOutStatementFactory(); + protected abstract TranspilerConfiguration getConfiguration(); protected record BuildResult(BodyNode bodyNode, TokenList tokenList) {} @@ -37,10 +36,14 @@ public abstract class BodyTranspilerTests { return new BuildResult(bodyNode, tokenList); } + protected BodyTranspiler getBodyTranspiler() { + return this.getConfiguration().getBodyTranspiler(); + } + @Test public void smokeScreen() { assertDoesNotThrow(() -> { - getBodyTranspiler(); + this.getBodyTranspiler(); }); } @@ -49,10 +52,11 @@ public abstract class BodyTranspilerTests { final var source = "Hello, $target!"; final var buildResult = this.build(source); final var transpiler = this.getBodyTranspiler(); - final var outStatementFactory = this.getOutStatementFactory(); + final var state = TranspilerUtil.TranspilerState.withDefaultRootScope(); + final var addOrAppend = this.getConfiguration().getAppendOrAddStatementFactory(); final BlockStatement blockStatement = transpiler.transpileBody( buildResult.bodyNode(), - (ignored, expression) -> outStatementFactory.create(expression), + (node, expression) -> addOrAppend.addOrAppend(node, state, ignored -> expression), TranspilerUtil.TranspilerState.withDefaultRootScope() ); assertEquals(1, blockStatement.getStatements().size()); @@ -63,10 +67,11 @@ public abstract class BodyTranspilerTests { final var source = "Hello, World!"; final var buildResult = this.build(source); final var transpiler = this.getBodyTranspiler(); - final var outStatementFactory = this.getOutStatementFactory(); + final var state = TranspilerUtil.TranspilerState.withDefaultRootScope(); + final var addOrAppend = this.getConfiguration().getAppendOrAddStatementFactory(); final BlockStatement blockStatement = transpiler.transpileBody( buildResult.bodyNode(), - (ignored, expression) -> outStatementFactory.create(expression), + (node, expression) -> addOrAppend.addOrAppend(node, state, ignored -> expression), TranspilerUtil.TranspilerState.withDefaultRootScope() ); assertEquals(1, blockStatement.getStatements().size());