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
implements ComponentFactory<T> {
private static final String DO_CREATE = "doCreate";
private static final Class<?>[] EMPTY_CLASSES = new Class[0];
protected final Map<Class<?>[], MetaMethod> cache = new HashMap<>();
private 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);
}
}
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)
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<T extends ViewComponent> 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);
}
}

View File

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

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 {
@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<ComponentScope> getScopeStack();

View File

@ -7,14 +7,14 @@ import java.util.function.Supplier;
public interface ComponentFactory<T extends ViewComponent> extends GroovyObject {
static <T extends ViewComponent> ComponentFactory<T> of(Closure<T> closure) {
return new DelegatingComponentFactory<>((context, args) -> closure.call(context, args));
static <T extends ViewComponent> ComponentFactory<T> ofClosure(Closure<T> closure) {
return new ClosureComponentFactory<>(closure);
}
static <T extends ViewComponent> ComponentFactory<T> of(Supplier<T> supplier) {
return new DelegatingComponentFactory<>((ignored0, ignored1) -> supplier.get());
static <T extends ViewComponent> ComponentFactory<T> ofSupplier(Supplier<T> supplier) {
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 {
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<ViewComponent> 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()");

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

View File

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

View File

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

View File

@ -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<WebViewChildRenderer> getChildren() {
return Objects.requireNonNullElseGet(this.children, ArrayList::new);
public List<WebViewChildRenderer> getChildRenderers() {
if (this.children == null) {
this.children = new ArrayList<>();
}
return this.children;
}
@Override
public void setChildren(List<WebViewChildRenderer> children) {
public boolean hasChildren() {
return !this.getChildRenderers().isEmpty();
}
@Override
public void setChildRenderers(List<WebViewChildRenderer> 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());
}
}
}

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;
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<? extends ViewComponent> forClass, Reader reader) {
protected ComponentTemplate doCompile(@Nullable Class<? extends ViewComponent> forClass, Reader reader) {
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);
// 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);
}
}

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) {
return this.nodeFactory.fragmentComponentNode(
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
public FragmentComponentNode fragmentComponentNode(TokenRange tokenRange, @Nullable BodyNode bodyNode) {
public FragmentComponentNode fragmentComponentNode(TokenRange tokenRange, BodyNode bodyNode) {
return this.objectFactory.get(FragmentComponentNode.class, tokenRange, bodyNode);
}

View File

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

View File

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

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

View File

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

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.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");

View File

@ -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<ClosureExpression, Variable> 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<MethodCallExpression, @Nullable Variable> 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<AttrNode> 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<ExpressionStatement, @Nullable Variable> 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;
}

View File

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

View File

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

View File

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

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 {
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 CONTEXT = "context";
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) {
return new Tuple2<>(
@ -58,6 +60,7 @@ public final class TranspilerUtil {
private final AtomicInteger componentCounter = new AtomicInteger();
private final Deque<VariableScope> scopeStack = new LinkedList<>();
private final Deque<Variable> 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() {}

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

View File

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

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.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());