Fragments and child rendering are working.

This commit is contained in:
JesseBrault0709 2024-05-04 12:55:42 +02:00
parent f573f1cec4
commit ea4c29f1d7
46 changed files with 793 additions and 281 deletions

View File

@ -9,50 +9,38 @@ import java.util.*;
public abstract class AbstractComponentFactory<T extends ViewComponent> extends GroovyObjectSupport public abstract class AbstractComponentFactory<T extends ViewComponent> extends GroovyObjectSupport
implements ComponentFactory<T> { implements ComponentFactory<T> {
private static final String DO_CREATE = "doCreate"; protected final Map<Class<?>[], MetaMethod> cache = new HashMap<>();
private static final Class<?>[] EMPTY_CLASSES = new Class[0];
private static Object[] flatten(Object... args) { protected MetaMethod findDoCreateMethod(Object[] allArgs) {
if (args.length == 0) { return this.cache.computeIfAbsent(ComponentFactoryUtil.asTypes(allArgs), types ->
return args; ComponentFactoryUtil.findDoCreateMethod(this.getMetaClass(), types)
} else {
final List<Object> 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<Class<?>[], MetaMethod> cache = new HashMap<>();
private MetaMethod findDoCreateMethod(Object[] allArgs) {
return this.cache.computeIfAbsent(asTypes(allArgs), types ->
this.getMetaClass().getMetaMethod(DO_CREATE, types)
); );
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private T findAndDoCreate(ComponentContext componentContext, Object[] args) { protected T findAndDoCreate(Object type, ComponentContext componentContext, Object[] args) {
final Object[] contextsAndArgs = flatten(componentContext, args); final Object[] typeContextAndArgs = ComponentFactoryUtil.flatten(type, componentContext, args);
final MetaMethod contextsAndArgsMethod = this.findDoCreateMethod(contextsAndArgs); final MetaMethod typeContextAndArgsMethod = this.findDoCreateMethod(typeContextAndArgs);
if (contextsAndArgsMethod != null) { if (typeContextAndArgsMethod != null) {
return (T) contextsAndArgsMethod.invoke(this, contextsAndArgs); 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 }; final Object[] contextOnly = new Object[] { componentContext };
@ -67,15 +55,15 @@ public abstract class AbstractComponentFactory<T extends ViewComponent> extends
} }
throw new MissingMethodException( throw new MissingMethodException(
DO_CREATE, ComponentFactoryUtil.DO_CREATE,
this.getClass(), this.getClass(),
args args
); );
} }
@Override @Override
public T create(ComponentContext componentContext, Object... args) { public T create(Object type, ComponentContext componentContext, Object... args) {
return this.findAndDoCreate(componentContext, args); return this.findAndDoCreate(type, componentContext, args);
} }
} }

View File

@ -32,10 +32,12 @@ public abstract class AbstractViewComponent implements ViewComponent {
this.template = Objects.requireNonNull(template); this.template = Objects.requireNonNull(template);
} }
protected void beforeRender() {} protected void beforeRender() {
this.getContext().beforeComponentRender(this);
}
protected void afterRender() { protected void afterRender() {
this.getContext().afterComponent(this); this.getContext().afterComponentRender(this);
} }
@Override @Override

View File

@ -0,0 +1,19 @@
package groowt.view.component;
import groovy.lang.Closure;
final class ClosureComponentFactory<T extends ViewComponent> extends AbstractComponentFactory<T> {
private final Closure<T> closure;
public ClosureComponentFactory(Closure<T> 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);
}
}

View File

@ -11,13 +11,21 @@ import java.util.function.Predicate;
public interface ComponentContext { public interface ComponentContext {
@ApiStatus.Internal @ApiStatus.Internal
ComponentFactory<?> resolve(String component); interface Resolved {
String getTypeName();
ComponentFactory<?> getComponentFactory();
}
@ApiStatus.Internal @ApiStatus.Internal
ViewComponent create(ComponentFactory<?> factory, Object... args); Resolved resolve(String component);
@ApiStatus.Internal @ApiStatus.Internal
void afterComponent(ViewComponent component); ViewComponent create(Resolved resolved, Object... args);
void beforeComponentRender(ViewComponent component);
@ApiStatus.Internal
void afterComponentRender(ViewComponent component);
Deque<ComponentScope> getScopeStack(); Deque<ComponentScope> getScopeStack();

View File

@ -7,14 +7,14 @@ import java.util.function.Supplier;
public interface ComponentFactory<T extends ViewComponent> extends GroovyObject { public interface ComponentFactory<T extends ViewComponent> extends GroovyObject {
static <T extends ViewComponent> ComponentFactory<T> of(Closure<T> closure) { static <T extends ViewComponent> ComponentFactory<T> ofClosure(Closure<T> closure) {
return new DelegatingComponentFactory<>((context, args) -> closure.call(context, args)); return new ClosureComponentFactory<>(closure);
} }
static <T extends ViewComponent> ComponentFactory<T> of(Supplier<T> supplier) { static <T extends ViewComponent> ComponentFactory<T> ofSupplier(Supplier<T> supplier) {
return new DelegatingComponentFactory<>((ignored0, ignored1) -> supplier.get()); return new SupplierComponentFactory<>(supplier);
} }
T create(ComponentContext componentContext, Object... args); T create(Object type, ComponentContext componentContext, Object... args);
} }

View File

@ -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<Object> 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() {}
}

View File

@ -10,11 +10,33 @@ import java.util.function.Predicate;
public class DefaultComponentContext implements ComponentContext { 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<ComponentScope> scopeStack = new LinkedList<>(); private final Deque<ComponentScope> scopeStack = new LinkedList<>();
private final Deque<ViewComponent> componentStack = new LinkedList<>(); private final Deque<ViewComponent> componentStack = new LinkedList<>();
@Override @Override
public ComponentFactory<?> resolve(String component) { public Resolved resolve(String component) {
if (scopeStack.isEmpty()) { if (scopeStack.isEmpty()) {
throw new IllegalStateException("There are no scopes on the scopeStack."); throw new IllegalStateException("There are no scopes on the scopeStack.");
} }
@ -23,7 +45,7 @@ public class DefaultComponentContext implements ComponentContext {
while (!getStack.isEmpty()) { while (!getStack.isEmpty()) {
final ComponentScope scope = getStack.pop(); final ComponentScope scope = getStack.pop();
if (scope.contains(component)) { 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()) { while (!missingStack.isEmpty()) {
final ComponentScope scope = getStack.pop(); final ComponentScope scope = getStack.pop();
try { try {
return scope.factoryMissing(component); return new DefaultResolved(component, scope.factoryMissing(component));
} catch (NoFactoryMissingException e) { } catch (NoFactoryMissingException e) {
if (first == null) { if (first == null) {
first = e; first = e;
@ -48,14 +70,19 @@ public class DefaultComponentContext implements ComponentContext {
} }
@Override @Override
public ViewComponent create(ComponentFactory<?> factory, Object... args) { public ViewComponent create(Resolved resolved, Object... args) {
final ViewComponent component = factory.create(this, args); return resolved.getComponentFactory().create(
this.componentStack.push(component); resolved.getTypeName(), this, args
return component; );
} }
@Override @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(); final var popped = this.componentStack.pop();
if (!popped.equals(component)) { if (!popped.equals(component)) {
throw new IllegalStateException("Popped component does not equal arg to afterComponent()"); throw new IllegalStateException("Popped component does not equal arg to afterComponent()");

View File

@ -1,20 +0,0 @@
package groowt.view.component;
final class DelegatingComponentFactory<T extends ViewComponent> extends AbstractComponentFactory<T> {
@FunctionalInterface
interface ComponentFactoryDelegate<T extends ViewComponent> {
T doCreate(ComponentContext context, Object... args);
}
private final ComponentFactoryDelegate<T> function;
public DelegatingComponentFactory(ComponentFactoryDelegate<T> function) {
this.function = function;
}
public T doCreate(ComponentContext componentContext, Object... args) {
return this.function.doCreate(componentContext, args);
}
}

View File

@ -0,0 +1,17 @@
package groowt.view.component;
import java.util.function.Supplier;
final class SupplierComponentFactory<T extends ViewComponent> extends AbstractComponentFactory<T> {
private final Supplier<T> tSupplier;
public SupplierComponentFactory(Supplier<T> tSupplier) {
this.tSupplier = tSupplier;
}
public T doCreate(Object type, ComponentContext componentContext, Object... args) {
return this.tSupplier.get();
}
}

View File

@ -1,6 +1,7 @@
package groowt.view.component; package groowt.view.component;
import groowt.view.View; import groowt.view.View;
import org.jetbrains.annotations.ApiStatus;
public interface ViewComponent extends View { public interface ViewComponent extends View {
@ -8,7 +9,9 @@ public interface ViewComponent extends View {
return this.getClass().getName(); return this.getClass().getName();
} }
@ApiStatus.Internal
void setContext(ComponentContext context); void setContext(ComponentContext context);
ComponentContext getContext(); ComponentContext getContext();
} }

View File

@ -0,0 +1,3 @@
<>
<Greeter greeting='Hello, one!' />&nbsp;<Greeter greeting='Hello, two!' />
</>

View File

@ -56,7 +56,7 @@ closingComponent
fragmentComponent fragmentComponent
: ComponentOpen ComponentClose : ComponentOpen ComponentClose
body? body
ClosingComponentOpen ComponentClose ClosingComponentOpen ComponentClose
; ;

View File

@ -2,13 +2,11 @@ package groowt.view.web;
import groovy.lang.Closure; import groovy.lang.Closure;
import groovy.lang.GroovyClassLoader; import groovy.lang.GroovyClassLoader;
import groowt.util.di.DefaultRegistryObjectFactory;
import groowt.view.component.AbstractViewComponent; import groowt.view.component.AbstractViewComponent;
import groowt.view.component.ComponentContext; import groowt.view.component.ComponentContext;
import groowt.view.component.ComponentTemplate; import groowt.view.component.ComponentTemplate;
import groowt.view.web.WebViewTemplateComponentSource.*; import groowt.view.web.WebViewTemplateComponentSource.*;
import groowt.view.web.runtime.WebViewComponentWriter; import groowt.view.web.runtime.WebViewComponentWriter;
import groowt.view.web.transpile.DefaultTranspilerConfiguration;
import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.CompilerConfiguration;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -77,25 +75,36 @@ public class DefaultWebViewComponent extends AbstractViewComponent implements We
} }
@Override @Override
public List<WebViewChildRenderer> getChildren() { public List<WebViewChildRenderer> getChildRenderers() {
return Objects.requireNonNullElseGet(this.children, ArrayList::new); if (this.children == null) {
this.children = new ArrayList<>();
}
return this.children;
} }
@Override @Override
public void setChildren(List<WebViewChildRenderer> children) { public boolean hasChildren() {
return !this.getChildRenderers().isEmpty();
}
@Override
public void setChildRenderers(List<WebViewChildRenderer> children) {
this.children = children; this.children = children;
} }
@Override @Override
public void renderChildren() { public void renderChildren() {
for (final var childRenderer : this.getChildren()) { for (final var childRenderer : this.getChildRenderers()) {
try { try {
if (childRenderer instanceof WebViewChildComponentRenderer childComponentRenderer) {
this.getContext().beforeComponentRender(childComponentRenderer.getComponent());
}
childRenderer.render(this); childRenderer.render(this);
} catch (Exception e) { } catch (Exception e) {
throw new ChildRenderException(e); throw new ChildRenderException(e);
} finally { } finally {
if (childRenderer instanceof WebViewChildComponentRenderer childComponentRenderer) { if (childRenderer instanceof WebViewChildComponentRenderer childComponentRenderer) {
this.getContext().afterComponent(childComponentRenderer.getComponent()); this.getContext().afterComponentRender(childComponentRenderer.getComponent());
} }
} }
} }

View File

@ -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
}
}

View File

@ -0,0 +1,24 @@
package groowt.view.web;
import groowt.view.component.ViewComponent;
import java.util.List;
public interface WebViewComponent extends ViewComponent {
List<WebViewChildRenderer> getChildRenderers();
boolean hasChildren();
void setChildRenderers(List<WebViewChildRenderer> children);
void renderChildren();
default List<Object> 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();
}
}

View File

@ -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
}
}

View File

@ -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<String, Object> attr;
public DelegatingWebViewComponent(Map<String, Object> attr) {
this.attr = attr;
}
protected Map<String, Object> getAttr() {
return this.attr;
}
protected abstract View getDelegate();
@Override
public void renderTo(Writer out) throws IOException {
this.getDelegate().renderTo(out);
}
}

View File

@ -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> {
Echo doCreate(String typeName, ComponentContext context, Map<String, Object> attr) {
doCreate(typeName, context, attr, true)
}
Echo doCreate(String typeName, ComponentContext context, Map<String, Object> attr, boolean selfClose) {
def echo = new Echo(attr, typeName, selfClose)
echo.context = context
echo
}
Echo doCreate(
String typeName,
ComponentContext context,
Map<String, Object> attr,
List<WebViewChildComponentRenderer> 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('<Echo> can only be used with String types.')
}
if (args == null || args.length < 1) {
throw new IllegalArgumentException(
'<Echo> 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<String, Object> 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 << ' '
}
}
}
}

View File

@ -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()
}
}

View File

@ -1,7 +1,6 @@
package groowt.view.web; package groowt.view.web;
import groovy.lang.GroovyClassLoader; import groovy.lang.GroovyClassLoader;
import groowt.util.di.RegistryObjectFactory;
import groowt.view.component.CachingComponentTemplateCompiler; import groowt.view.component.CachingComponentTemplateCompiler;
import groowt.view.component.ComponentTemplate; import groowt.view.component.ComponentTemplate;
import groowt.view.component.ComponentTemplateCreateException; 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.ast.node.CompilationUnitNode;
import groowt.view.web.transpile.DefaultGroovyTranspiler; import groowt.view.web.transpile.DefaultGroovyTranspiler;
import groowt.view.web.transpile.DefaultTranspilerConfiguration; import groowt.view.web.transpile.DefaultTranspilerConfiguration;
import groowt.view.web.transpile.TranspilerConfiguration;
import org.codehaus.groovy.control.CompilationUnit; import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.Phases; import org.codehaus.groovy.control.Phases;
import org.codehaus.groovy.control.io.AbstractReaderSource; 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.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Objects; import java.util.Objects;
import java.util.function.Supplier;
public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplateCompiler { public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplateCompiler {
@ -74,11 +70,11 @@ public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplat
this.groovyClassLoader = null; this.groovyClassLoader = null;
} }
protected ComponentTemplate doCompile(Class<? extends ViewComponent> forClass, Reader reader) { protected ComponentTemplate doCompile(@Nullable Class<? extends ViewComponent> forClass, Reader reader) {
return this.doCompile(forClass, reader, null); return this.doCompile(forClass, reader, null);
} }
protected ComponentTemplate doCompile(Class<? extends ViewComponent> forClass, Reader reader, @Nullable URI uri) { protected ComponentTemplate doCompile(@Nullable Class<? extends ViewComponent> forClass, Reader reader, @Nullable URI uri) {
final CompilationUnitParseResult parseResult = ParserUtil.parseCompilationUnit(reader); final CompilationUnitParseResult parseResult = ParserUtil.parseCompilationUnit(reader);
// TODO: analysis // TODO: analysis
@ -94,7 +90,7 @@ public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplat
DefaultTranspilerConfiguration::new DefaultTranspilerConfiguration::new
); );
final var ownerComponentName = forClass.getSimpleName(); final var ownerComponentName = forClass != null ? forClass.getSimpleName() : "AnonymousComponent";
final var templateClassName = ownerComponentName + "Template"; final var templateClassName = ownerComponentName + "Template";
final var fqn = this.defaultPackageName + "." + templateClassName; final var fqn = this.defaultPackageName + "." + templateClassName;
@ -205,4 +201,8 @@ public class DefaultWebComponentTemplateCompiler extends CachingComponentTemplat
return this.getFromCacheOrElse(forClass, () -> this.doCompile(forClass, reader)); return this.getFromCacheOrElse(forClass, () -> this.doCompile(forClass, reader));
} }
public ComponentTemplate compileAnonymous(Reader reader) {
return this.doCompile(null, reader);
}
} }

View File

@ -1,11 +0,0 @@
package groowt.view.web;
import groowt.view.component.ViewComponent;
import java.util.List;
public interface WebViewComponent extends ViewComponent {
List<WebViewChildRenderer> getChildren();
void setChildren(List<WebViewChildRenderer> children);
void renderChildren();
}

View File

@ -189,7 +189,7 @@ public class DefaultAstBuilderVisitor extends WebViewComponentsParserBaseVisitor
public Node visitFragmentComponent(WebViewComponentsParser.FragmentComponentContext ctx) { public Node visitFragmentComponent(WebViewComponentsParser.FragmentComponentContext ctx) {
return this.nodeFactory.fragmentComponentNode( return this.nodeFactory.fragmentComponentNode(
this.getTokenRange(ctx), this.getTokenRange(ctx),
this.getSingleAs(ctx.body(), BodyNode.class) this.getSingleAsNonNull(ctx.body(), BodyNode.class)
); );
} }

View File

@ -125,7 +125,7 @@ public class DefaultNodeFactory implements NodeFactory {
} }
@Override @Override
public FragmentComponentNode fragmentComponentNode(TokenRange tokenRange, @Nullable BodyNode bodyNode) { public FragmentComponentNode fragmentComponentNode(TokenRange tokenRange, BodyNode bodyNode) {
return this.objectFactory.get(FragmentComponentNode.class, tokenRange, bodyNode); return this.objectFactory.get(FragmentComponentNode.class, tokenRange, bodyNode);
} }

View File

@ -28,7 +28,7 @@ public interface NodeFactory {
@Nullable BodyNode body @Nullable BodyNode body
); );
FragmentComponentNode fragmentComponentNode(TokenRange tokenRange, @Nullable BodyNode bodyNode); FragmentComponentNode fragmentComponentNode(TokenRange tokenRange, BodyNode bodyNode);
ComponentArgsNode componentArgsNode( ComponentArgsNode componentArgsNode(
TokenRange tokenRange, TokenRange tokenRange,

View File

@ -4,7 +4,9 @@ import groowt.util.di.annotation.Given;
import groowt.view.web.ast.extension.NodeExtensionContainer; import groowt.view.web.ast.extension.NodeExtensionContainer;
import groowt.view.web.util.TokenRange; import groowt.view.web.util.TokenRange;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Objects;
public non-sealed class FragmentComponentNode extends ComponentNode { public non-sealed class FragmentComponentNode extends ComponentNode {
@ -12,9 +14,14 @@ public non-sealed class FragmentComponentNode extends ComponentNode {
public FragmentComponentNode( public FragmentComponentNode(
NodeExtensionContainer extensionContainer, NodeExtensionContainer extensionContainer,
@Given TokenRange tokenRange, @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());
} }
} }

View File

@ -1,8 +0,0 @@
package groowt.view.web.lib;
import groowt.view.web.DefaultWebViewComponent;
// TODO: anything special?
public class FragmentComponent extends DefaultWebViewComponent {
}

View File

@ -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<Action, Expression> getRightSide);
}

View File

@ -1,25 +1,22 @@
package groowt.view.web.transpile; 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.BodyNode;
import groowt.view.web.ast.node.Node;
import groowt.view.web.transpile.TranspilerUtil.TranspilerState; 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.expr.Expression;
import org.codehaus.groovy.ast.stmt.BlockStatement; import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.Statement; import org.codehaus.groovy.ast.stmt.Statement;
import java.util.List;
public interface BodyTranspiler { public interface BodyTranspiler {
@FunctionalInterface @FunctionalInterface
interface ExpressionStatementConverter { interface AddOrAppendCallback {
Statement createStatement(Node source, Expression expression); Statement createStatement(BodyChildNode source, Expression expression);
} }
BlockStatement transpileBody( BlockStatement transpileBody(
BodyNode bodyNode, BodyNode bodyNode,
ExpressionStatementConverter converter, AddOrAppendCallback addOrAppendCallback,
TranspilerState state TranspilerState state
); );

View File

@ -2,10 +2,7 @@ package groowt.view.web.transpile;
import groowt.view.web.ast.node.ComponentNode; import groowt.view.web.ast.node.ComponentNode;
import groowt.view.web.transpile.TranspilerUtil.TranspilerState; 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.BlockStatement;
import org.codehaus.groovy.ast.stmt.Statement;
public interface ComponentTranspiler { public interface ComponentTranspiler {
BlockStatement createComponentStatements( BlockStatement createComponentStatements(

View File

@ -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<ConstantExpression, ConstantExpression> 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<Action, Expression> getRightSide
) {
if (state.hasCurrentChildCollector()) {
return this.addOnly(bodyChildNode, state, getRightSide.apply(Action.ADD));
} else {
return this.appendOnly(bodyChildNode, state, getRightSide.apply(Action.APPEND));
}
}
}

View File

@ -3,15 +3,9 @@ package groowt.view.web.transpile;
import groowt.view.web.ast.node.*; import groowt.view.web.ast.node.*;
import groowt.view.web.transpile.TranspilerUtil.TranspilerState; import groowt.view.web.transpile.TranspilerUtil.TranspilerState;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.codehaus.groovy.ast.expr.GStringExpression; import org.codehaus.groovy.ast.expr.GStringExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement; 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 { public class DefaultBodyTranspiler implements BodyTranspiler {
private final GStringTranspiler gStringTranspiler; private final GStringTranspiler gStringTranspiler;
@ -32,7 +26,7 @@ public class DefaultBodyTranspiler implements BodyTranspiler {
@Override @Override
public BlockStatement transpileBody( public BlockStatement transpileBody(
BodyNode bodyNode, BodyNode bodyNode,
ExpressionStatementConverter converter, AddOrAppendCallback addOrAppendCallback,
TranspilerState state TranspilerState state
) { ) {
final BlockStatement block = new BlockStatement(); final BlockStatement block = new BlockStatement();
@ -43,22 +37,19 @@ public class DefaultBodyTranspiler implements BodyTranspiler {
final GStringExpression gString = this.gStringTranspiler.createGStringExpression( final GStringExpression gString = this.gStringTranspiler.createGStringExpression(
gStringBodyTextNode gStringBodyTextNode
); );
block.addStatement(converter.createStatement(gStringBodyTextNode, gString)); block.addStatement(addOrAppendCallback.createStatement(gStringBodyTextNode, gString));
} }
case JStringBodyTextNode jStringBodyTextNode -> { case JStringBodyTextNode jStringBodyTextNode -> {
block.addStatement( block.addStatement(
converter.createStatement( addOrAppendCallback.createStatement(
jStringBodyTextNode, jStringBodyTextNode,
this.jStringTranspiler.createStringLiteral(jStringBodyTextNode) this.jStringTranspiler.createStringLiteral(jStringBodyTextNode)
) )
); );
} }
case ComponentNode componentNode -> { case ComponentNode componentNode -> {
final BlockStatement componentBlock = this.componentTranspiler.createComponentStatements( // DO NOT add/append this, because the component transpiler does it already
componentNode, block.addStatement(this.componentTranspiler.createComponentStatements(componentNode, state));
state
);
block.addStatement(componentBlock);
} }
case PlainScriptletNode plainScriptletNode -> { case PlainScriptletNode plainScriptletNode -> {
throw new UnsupportedOperationException("TODO"); throw new UnsupportedOperationException("TODO");

View File

@ -1,10 +1,9 @@
package groowt.view.web.transpile; package groowt.view.web.transpile;
import groovy.lang.Tuple2;
import groowt.view.component.*; import groowt.view.component.*;
import groowt.view.web.ast.node.*; import groowt.view.web.ast.node.*;
import groowt.view.web.lib.FragmentComponent;
import groowt.view.web.runtime.WebViewComponentChildCollector; 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;
import groowt.view.web.transpile.util.GroovyUtil.ConvertResult; import groowt.view.web.transpile.util.GroovyUtil.ConvertResult;
import org.codehaus.groovy.ast.*; import org.codehaus.groovy.ast.*;
@ -16,9 +15,9 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import static groowt.view.web.transpile.TranspilerUtil.lineAndColumn; import static groowt.view.web.transpile.TranspilerUtil.*;
import static groowt.view.web.transpile.TranspilerUtil.makeStringLiteral;
public class DefaultComponentTranspiler implements ComponentTranspiler { 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_COMPONENT_EXCEPTION = ClassHelper.make(MissingComponentException.class);
private static final ClassNode MISSING_CLASS_TYPE_EXCEPTION = ClassHelper.make(MissingClassTypeException.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_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 = "create";
private static final String CREATE_FRAGMENT = "createFragment";
private static final String RESOLVE = "resolve"; private static final String RESOLVE = "resolve";
private static final String ADD = "add"; private static final String ADD = "add";
private static final String APPEND = "append"; 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 ValueNodeTranspiler valueNodeTranspiler;
private BodyTranspiler bodyTranspiler; private BodyTranspiler bodyTranspiler;
private AppendOrAddStatementFactory appendOrAddStatementFactory;
public void setValueNodeTranspiler(ValueNodeTranspiler valueNodeTranspiler) { public void setValueNodeTranspiler(ValueNodeTranspiler valueNodeTranspiler) {
this.valueNodeTranspiler = valueNodeTranspiler; this.valueNodeTranspiler = valueNodeTranspiler;
@ -52,6 +54,10 @@ public class DefaultComponentTranspiler implements ComponentTranspiler {
this.bodyTranspiler = bodyTranspiler; this.bodyTranspiler = bodyTranspiler;
} }
public void setAppendOrAddStatementFactory(AppendOrAddStatementFactory appendOrAddStatementFactory) {
this.appendOrAddStatementFactory = Objects.requireNonNull(appendOrAddStatementFactory);
}
// ViewComponent c0 // ViewComponent c0
protected ExpressionStatement getComponentDeclaration(Variable component) { protected ExpressionStatement getComponentDeclaration(Variable component) {
final var componentDeclaration = new DeclarationExpression( final var componentDeclaration = new DeclarationExpression(
@ -82,9 +88,7 @@ public class DefaultComponentTranspiler implements ComponentTranspiler {
} }
// key: value // key: value
protected MapEntryExpression getAttrExpression( protected MapEntryExpression getAttrExpression(AttrNode attrNode, TranspilerState state) {
AttrNode attrNode, TranspilerState state
) {
final var keyExpr = makeStringLiteral(attrNode.getKeyNode().getKey()); final var keyExpr = makeStringLiteral(attrNode.getKeyNode().getKey());
final Expression valueExpr = switch (attrNode) { final Expression valueExpr = switch (attrNode) {
case BooleanValueAttrNode ignored -> ConstantExpression.PRIM_TRUE; case BooleanValueAttrNode ignored -> ConstantExpression.PRIM_TRUE;
@ -129,13 +133,13 @@ public class DefaultComponentTranspiler implements ComponentTranspiler {
args.addExpression(lineAndColumn.getV2()); 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 VariableExpression outVariableExpr = new VariableExpression(state.out());
final ArgumentListExpression args = new ArgumentListExpression(); final ArgumentListExpression args = new ArgumentListExpression();
args.addExpression(toOutput); args.addExpression(toOutput);
switch (sourceNode) { switch (sourceNode) {
case GStringBodyTextNode ignored -> this.addLineAndColumn(sourceNode, args); case GStringBodyTextNode ignored -> this.addLineAndColumn(sourceNode.asNode(), args);
case ComponentNode ignored -> this.addLineAndColumn(sourceNode, args); case ComponentNode ignored -> this.addLineAndColumn(sourceNode.asNode(), args);
default -> { default -> {
} }
} }
@ -143,7 +147,7 @@ public class DefaultComponentTranspiler implements ComponentTranspiler {
} }
// { out << jString | gString | component } // { 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) { if (toRender instanceof VariableExpression variableExpression) {
variableExpression.setClosureSharedVariable(true); variableExpression.setClosureSharedVariable(true);
} }
@ -153,7 +157,7 @@ public class DefaultComponentTranspiler implements ComponentTranspiler {
// c0_childCollector.add (jString | gString | component) { out << ... } // c0_childCollector.add (jString | gString | component) { out << ... }
protected Statement getChildCollectorAdd( protected Statement getChildCollectorAdd(
Node sourceNode, BodyChildNode sourceNode,
TranspilerState state, TranspilerState state,
Variable childCollector, Variable childCollector,
Expression toAdd Expression toAdd
@ -168,8 +172,15 @@ public class DefaultComponentTranspiler implements ComponentTranspiler {
return new ExpressionStatement(methodCall); return new ExpressionStatement(methodCall);
} }
/**
* @return Tuple containing 1. body ClosureExpression, and 2. childCollector Variable
*/
// { WebViewComponentChildCollector c0_childCollector -> ... } // { WebViewComponentChildCollector c0_childCollector -> ... }
protected ClosureExpression getBodyClosure(BodyNode bodyNode, TranspilerState state, String componentVariableName) { protected Tuple2<ClosureExpression, Variable> getBodyClosure(
BodyNode bodyNode,
TranspilerState state,
String componentVariableName
) {
final Parameter childCollectorParam = new Parameter( final Parameter childCollectorParam = new Parameter(
CHILD_COLLECTOR, CHILD_COLLECTOR,
componentVariableName + "_childCollector" componentVariableName + "_childCollector"
@ -177,26 +188,40 @@ public class DefaultComponentTranspiler implements ComponentTranspiler {
final var scope = state.pushScope(); final var scope = state.pushScope();
scope.putDeclaredVariable(childCollectorParam); scope.putDeclaredVariable(childCollectorParam);
state.pushChildCollector(childCollectorParam);
final BlockStatement bodyStatements = this.bodyTranspiler.transpileBody( final BlockStatement bodyStatements = this.bodyTranspiler.transpileBody(
bodyNode, bodyNode,
(sourceNode, expr) -> this.getChildCollectorAdd(sourceNode, state, childCollectorParam, expr), (sourceNode, expr) -> this.getChildCollectorAdd(sourceNode, state, childCollectorParam, expr),
state state
); );
state.popChildCollector();
state.popScope(); 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(...) {...} // context.create(...) {...}
protected MethodCallExpression getCreateExpression( protected Tuple2<MethodCallExpression, @Nullable Variable> getCreateExpression(
ComponentNode componentNode, TranspilerState state, String componentVariableName ComponentNode componentNode,
TranspilerState state,
String componentVariableName
) { ) {
final var createArgs = new ArgumentListExpression(); final var createArgs = new ArgumentListExpression();
final String createName;
final var contextResolve = this.getContextResolveExpr(componentNode, state.context()); Variable childCollector = null;
createArgs.addExpression(contextResolve);
if (componentNode instanceof TypedComponentNode typedComponentNode) { if (componentNode instanceof TypedComponentNode typedComponentNode) {
createName = CREATE;
final var contextResolve = this.getContextResolveExpr(componentNode, state.context());
createArgs.addExpression(contextResolve);
final List<AttrNode> attributeNodes = typedComponentNode.getArgs().getAttributes(); final List<AttrNode> attributeNodes = typedComponentNode.getArgs().getAttributes();
if (!attributeNodes.isEmpty()) { if (!attributeNodes.isEmpty()) {
createArgs.addExpression(this.getAttrMap(attributeNodes, state)); createArgs.addExpression(this.getAttrMap(attributeNodes, state));
@ -205,35 +230,62 @@ public class DefaultComponentTranspiler implements ComponentTranspiler {
if (constructorNode != null) { if (constructorNode != null) {
this.getConstructorArgs(constructorNode).forEach(createArgs::addExpression); 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(); final var createCall = new MethodCallExpression(
if (bodyNode != null) { new VariableExpression(state.context()),
createArgs.addExpression(this.getBodyClosure(bodyNode, state, componentVariableName)); 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(''), [:], ...) {...} // c0 = context.create(context.resolve(''), [:], ...) {...}
protected ExpressionStatement getCreateAssignStatement( protected Tuple2<ExpressionStatement, @Nullable Variable> getCreateAssignStatement(
ComponentNode componentNode, TranspilerState state, String componentVariableName ComponentNode componentNode,
TranspilerState state,
String componentVariableName,
Variable component
) { ) {
final var componentAssignLeft = new VariableExpression(state.getDeclaredVariable(componentVariableName)); final var componentAssignLeft = new VariableExpression(component);
final var createExpr = this.getCreateExpression(componentNode, state, componentVariableName); final var createExprResult = this.getCreateExpression(componentNode, state, componentVariableName);
final var componentAssignExpr = new BinaryExpression( final var componentAssignExpr = new BinaryExpression(
componentAssignLeft, componentAssignLeft,
new Token(Types.ASSIGN, "=", -1, -1), new Token(Types.ASSIGN, "=", -1, -1),
createExpr createExprResult.getV1()
); );
return new ExpressionStatement(componentAssignExpr); return new Tuple2<>(new ExpressionStatement(componentAssignExpr), createExprResult.getV2());
} }
// catch (NoFactoryMissingException c0nfme) { // catch (NoFactoryMissingException c0nfme) {
// throw new MissingClassComponentException(this, 'ComponentType', c0nfme) // throw new MissingClassComponentException(this, 'ComponentType', c0nfme)
// } // }
protected CatchStatement getNoMissingFactoryExceptionCatch( protected CatchStatement getNoMissingFactoryExceptionCatch(
ComponentNode componentNode, String componentVariableName ComponentNode componentNode,
String componentVariableName
) { ) {
final String exceptionName = componentVariableName + "nfme"; final String exceptionName = componentVariableName + "nfme";
final Parameter fmeParam = new Parameter(NO_FACTORY_MISSING_EXCEPTION, exceptionName); 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) } // 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 String exceptionName = componentVariableName + "ce";
final Parameter exceptionParam = new Parameter(EXCEPTION, exceptionName); final Parameter exceptionParam = new Parameter(EXCEPTION, exceptionName);
final VariableExpression exceptionVar = new VariableExpression(exceptionName); final VariableExpression exceptionVar = new VariableExpression(exceptionName);
@ -317,53 +372,68 @@ public class DefaultComponentTranspiler implements ComponentTranspiler {
return new ExpressionStatement(setContext); 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) { protected String getComponentVariableName(int componentNumber) {
return "c" + componentNumber; return "c" + componentNumber;
} }
@Override @Override
public BlockStatement createComponentStatements( public BlockStatement createComponentStatements(ComponentNode componentNode, TranspilerState state) {
ComponentNode componentNode,
TranspilerState state
) {
final var componentVariableName = this.getComponentVariableName(state.newComponentNumber()); 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 BlockStatement result = new BlockStatement();
final VariableScope scope = state.pushScope(); final VariableScope scope = state.currentScope();
result.setVariableScope(scope); result.setVariableScope(scope);
scope.putDeclaredVariable(component); scope.putDeclaredVariable(component);
// ViewComponent c0; // ViewComponent c0;
result.addStatement(this.getComponentDeclaration(component)); result.addStatement(this.getComponentDeclaration(component));
// try { context.create(...) } catch { ... } // c0 = context.create(...) { ... }
final var tryCreateStatement = new TryCatchStatement(this.getCreateAssignStatement( final var createAssignStatementResult = this.getCreateAssignStatement(
componentNode, componentNode,
state, state,
componentVariableName componentVariableName,
), EmptyStatement.INSTANCE); component
);
// try { ... } catch { ... }
final var tryCreateStatement = new TryCatchStatement(
createAssignStatementResult.getV1(),
EmptyStatement.INSTANCE
);
this.getCreateCatches(componentNode, componentVariableName).forEach(tryCreateStatement::addCatch); this.getCreateCatches(componentNode, componentVariableName).forEach(tryCreateStatement::addCatch);
result.addStatement(tryCreateStatement); result.addStatement(tryCreateStatement);
// component.setContext(context) // component.setContext(context)
result.addStatement(this.createSetContext(state, component)); result.addStatement(this.createSetContext(state, component));
// out << component // out or collect
result.addStatement(this.createComponentOutCall(componentNode, state, component)); final var addOrAppend = this.appendOrAddStatementFactory.addOrAppend(
componentNode,
state.popScope(); 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; return result;
} }

View File

@ -12,8 +12,6 @@ import groowt.view.web.transpile.util.GroovyUtil;
import groowt.view.web.util.FilteringIterable; import groowt.view.web.util.FilteringIterable;
import groowt.view.web.util.Option; import groowt.view.web.util.Option;
import groowt.view.web.util.TokenRange; import groowt.view.web.util.TokenRange;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.antlr.v4.runtime.Token; import org.antlr.v4.runtime.Token;
import org.codehaus.groovy.ast.expr.*; import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.stmt.BlockStatement; import org.codehaus.groovy.ast.stmt.BlockStatement;
@ -26,13 +24,11 @@ import java.util.List;
import java.util.ListIterator; import java.util.ListIterator;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Singleton
public class DefaultGStringTranspiler implements GStringTranspiler { public class DefaultGStringTranspiler implements GStringTranspiler {
private final PositionSetter positionSetter; private final PositionSetter positionSetter;
private final JStringTranspiler jStringTranspiler; private final JStringTranspiler jStringTranspiler;
@Inject
public DefaultGStringTranspiler(PositionSetter positionSetter, JStringTranspiler jStringTranspiler) { public DefaultGStringTranspiler(PositionSetter positionSetter, JStringTranspiler jStringTranspiler) {
this.positionSetter = positionSetter; this.positionSetter = positionSetter;
this.jStringTranspiler = jStringTranspiler; this.jStringTranspiler = jStringTranspiler;

View File

@ -210,12 +210,17 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler {
final BodyNode bodyNode = compilationUnitNode.getBodyNode(); final BodyNode bodyNode = compilationUnitNode.getBodyNode();
if (bodyNode != null) { if (bodyNode != null) {
final var outStatementFactory = configuration.getOutStatementFactory(); final var appendOrAddStatementFactory = configuration.getAppendOrAddStatementFactory();
renderBlock.addStatement( renderBlock.addStatement(
configuration.getBodyTranspiler() configuration.getBodyTranspiler()
.transpileBody( .transpileBody(
compilationUnitNode.getBodyNode(), 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 state
) )
); );
@ -223,8 +228,8 @@ public class DefaultGroovyTranspiler implements GroovyTranspiler {
final ClosureExpression renderer = new ClosureExpression( final ClosureExpression renderer = new ClosureExpression(
new Parameter[] { new Parameter[] {
(Parameter) state.getDeclaredVariable(CONTEXT), (Parameter) state.context(),
(Parameter) state.getDeclaredVariable(OUT) (Parameter) state.out()
}, },
renderBlock renderBlock
); );

View File

@ -4,7 +4,7 @@ import jakarta.inject.Inject;
public class DefaultTranspilerConfiguration implements TranspilerConfiguration { public class DefaultTranspilerConfiguration implements TranspilerConfiguration {
private final OutStatementFactory outStatementFactory = new SimpleOutStatementFactory(); private final AppendOrAddStatementFactory appendOrAddStatementFactory = new DefaultAppendOrAddStatementFactory();
private final BodyTranspiler bodyTranspiler; private final BodyTranspiler bodyTranspiler;
@Inject @Inject
@ -13,10 +13,13 @@ public class DefaultTranspilerConfiguration implements TranspilerConfiguration {
final var jStringTranspiler = new DefaultJStringTranspiler(positionSetter); final var jStringTranspiler = new DefaultJStringTranspiler(positionSetter);
final var gStringTranspiler = new DefaultGStringTranspiler(positionSetter, jStringTranspiler); final var gStringTranspiler = new DefaultGStringTranspiler(positionSetter, jStringTranspiler);
final var componentTranspiler = new DefaultComponentTranspiler(); final var componentTranspiler = new DefaultComponentTranspiler();
this.bodyTranspiler = new DefaultBodyTranspiler(gStringTranspiler, jStringTranspiler, componentTranspiler);
componentTranspiler.setBodyTranspiler(this.bodyTranspiler);
final var valueNodeTranspiler = new DefaultValueNodeTranspiler(componentTranspiler); final var valueNodeTranspiler = new DefaultValueNodeTranspiler(componentTranspiler);
this.bodyTranspiler = new DefaultBodyTranspiler(gStringTranspiler, jStringTranspiler, componentTranspiler);
componentTranspiler.setBodyTranspiler(this.bodyTranspiler);
componentTranspiler.setValueNodeTranspiler(valueNodeTranspiler); componentTranspiler.setValueNodeTranspiler(valueNodeTranspiler);
componentTranspiler.setAppendOrAddStatementFactory(this.appendOrAddStatementFactory);
} }
@Override @Override
@ -25,8 +28,8 @@ public class DefaultTranspilerConfiguration implements TranspilerConfiguration {
} }
@Override @Override
public OutStatementFactory getOutStatementFactory() { public AppendOrAddStatementFactory getAppendOrAddStatementFactory() {
return this.outStatementFactory; return this.appendOrAddStatementFactory;
} }
} }

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -2,5 +2,5 @@ package groowt.view.web.transpile;
public interface TranspilerConfiguration { public interface TranspilerConfiguration {
BodyTranspiler getBodyTranspiler(); BodyTranspiler getBodyTranspiler();
OutStatementFactory getOutStatementFactory(); AppendOrAddStatementFactory getAppendOrAddStatementFactory();
} }

View File

@ -25,6 +25,8 @@ public final class TranspilerUtil {
public static final String OUT = "out"; public static final String OUT = "out";
public static final String CONTEXT = "context"; public static final String CONTEXT = "context";
public static final String GET_RENDERER = "getRenderer"; public static final String GET_RENDERER = "getRenderer";
public static final String APPEND = "append";
public static final String ADD = "add";
public static Tuple2<ConstantExpression, ConstantExpression> lineAndColumn(SourcePosition sourcePosition) { public static Tuple2<ConstantExpression, ConstantExpression> lineAndColumn(SourcePosition sourcePosition) {
return new Tuple2<>( return new Tuple2<>(
@ -58,6 +60,7 @@ public final class TranspilerUtil {
private final AtomicInteger componentCounter = new AtomicInteger(); private final AtomicInteger componentCounter = new AtomicInteger();
private final Deque<VariableScope> scopeStack = new LinkedList<>(); private final Deque<VariableScope> scopeStack = new LinkedList<>();
private final Deque<Variable> childCollectorStack = new LinkedList<>();
private TranspilerState(VariableScope rootScope) { private TranspilerState(VariableScope rootScope) {
this.scopeStack.push(rootScope); this.scopeStack.push(rootScope);
@ -103,6 +106,22 @@ public final class TranspilerUtil {
throw new NullPointerException("Cannot find variable: " + name); 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() {} private TranspilerUtil() {}

View File

@ -0,0 +1 @@
<$typeName ${renderAttr()}${selfClose ? ' /' : ''}>${renderChildren()}${!selfClose ? "</$typeName>" : ''}

View File

@ -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<String, Object> 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('<><Greeter greeting="Hello, World!" /></>', 'Hello, World!', this.greeterContext)
}
@Test
void multipleChildren() {
this.doTest(
'''
<>
<Greeter greeting='Hello, one!' />&nbsp;<Greeter greeting='Hello, two!' />
</>
'''.stripIndent(), 'Hello, one!&nbsp;Hello, two!', this.greeterContext
)
}
}

View File

@ -5,21 +5,8 @@ import groowt.view.web.transpile.*;
public class DefaultBodyTranspilerTests extends BodyTranspilerTests { public class DefaultBodyTranspilerTests extends BodyTranspilerTests {
@Override @Override
protected BodyTranspiler getBodyTranspiler() { protected TranspilerConfiguration getConfiguration() {
final var positionSetter = new SimplePositionSetter(); return new DefaultTranspilerConfiguration();
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();
} }
} }

View File

@ -100,11 +100,6 @@ public abstract class NodeFactoryTests {
assertNotNull(this.nodeFactory.fragmentComponentNode(this.getTokenRange(), bodyNode)); assertNotNull(this.nodeFactory.fragmentComponentNode(this.getTokenRange(), bodyNode));
} }
@Test
public void fragmentComponentNodeBodyNull() {
assertNotNull(this.nodeFactory.fragmentComponentNode(this.getTokenRange(), null));
}
@Test @Test
public void componentArgsNodeWithClassComponentType( public void componentArgsNodeWithClassComponentType(
@Mock ClassComponentTypeNode componentTypeNode, @Mock ClassComponentTypeNode componentTypeNode,

View File

@ -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);
}
}

View File

@ -7,7 +7,7 @@ import groowt.view.web.ast.DefaultNodeFactory;
import groowt.view.web.ast.node.BodyNode; import groowt.view.web.ast.node.BodyNode;
import groowt.view.web.ast.node.CompilationUnitNode; import groowt.view.web.ast.node.CompilationUnitNode;
import groowt.view.web.transpile.BodyTranspiler; 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 groowt.view.web.transpile.TranspilerUtil;
import org.codehaus.groovy.ast.expr.ConstantExpression; import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.MethodCallExpression; import org.codehaus.groovy.ast.expr.MethodCallExpression;
@ -20,8 +20,7 @@ import static org.junit.jupiter.api.Assertions.*;
public abstract class BodyTranspilerTests { public abstract class BodyTranspilerTests {
protected abstract BodyTranspiler getBodyTranspiler(); protected abstract TranspilerConfiguration getConfiguration();
protected abstract OutStatementFactory getOutStatementFactory();
protected record BuildResult(BodyNode bodyNode, TokenList tokenList) {} protected record BuildResult(BodyNode bodyNode, TokenList tokenList) {}
@ -37,10 +36,14 @@ public abstract class BodyTranspilerTests {
return new BuildResult(bodyNode, tokenList); return new BuildResult(bodyNode, tokenList);
} }
protected BodyTranspiler getBodyTranspiler() {
return this.getConfiguration().getBodyTranspiler();
}
@Test @Test
public void smokeScreen() { public void smokeScreen() {
assertDoesNotThrow(() -> { assertDoesNotThrow(() -> {
getBodyTranspiler(); this.getBodyTranspiler();
}); });
} }
@ -49,10 +52,11 @@ public abstract class BodyTranspilerTests {
final var source = "Hello, $target!"; final var source = "Hello, $target!";
final var buildResult = this.build(source); final var buildResult = this.build(source);
final var transpiler = this.getBodyTranspiler(); 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( final BlockStatement blockStatement = transpiler.transpileBody(
buildResult.bodyNode(), buildResult.bodyNode(),
(ignored, expression) -> outStatementFactory.create(expression), (node, expression) -> addOrAppend.addOrAppend(node, state, ignored -> expression),
TranspilerUtil.TranspilerState.withDefaultRootScope() TranspilerUtil.TranspilerState.withDefaultRootScope()
); );
assertEquals(1, blockStatement.getStatements().size()); assertEquals(1, blockStatement.getStatements().size());
@ -63,10 +67,11 @@ public abstract class BodyTranspilerTests {
final var source = "Hello, World!"; final var source = "Hello, World!";
final var buildResult = this.build(source); final var buildResult = this.build(source);
final var transpiler = this.getBodyTranspiler(); 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( final BlockStatement blockStatement = transpiler.transpileBody(
buildResult.bodyNode(), buildResult.bodyNode(),
(ignored, expression) -> outStatementFactory.create(expression), (node, expression) -> addOrAppend.addOrAppend(node, state, ignored -> expression),
TranspilerUtil.TranspilerState.withDefaultRootScope() TranspilerUtil.TranspilerState.withDefaultRootScope()
); );
assertEquals(1, blockStatement.getStatements().size()); assertEquals(1, blockStatement.getStatements().size());