Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions htmlflow-core/src/main/java/htmlflow/HtmlFlow.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ private static PreprocessingVisitor preprocessing(
* accumulated in the internal StringBuffer out.
*/
preView.getVisitor().resolve(null);
pre.getFirst().compile();
return pre;
}

Expand All @@ -94,6 +95,7 @@ private static PreprocessingVisitorMfe preprocessingMfe(
template.resolve(preView);
// second process
preView.getVisitor().resolve(null);
processView.getFirst().compile();
return processView;
}

Expand Down Expand Up @@ -123,6 +125,7 @@ private static PreprocessingVisitorAsync preprocessingAsync(
* accumulated in the internal StringBuffer out.
*/
preView.getVisitor().resolve(null);
pre.getFirst().compile();
return pre;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
*/
package htmlflow.continuations;

import htmlflow.continuations.codegen.ChainCompiler;
import htmlflow.continuations.codegen.ChainCompiler.Renderer;
import htmlflow.visitor.HtmlVisitor;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
Expand Down Expand Up @@ -54,6 +56,9 @@ public abstract class HtmlContinuation {
/** Next HtmlContinuation */
public final HtmlContinuation next;

/** The bytecode of the chain starting in this node, or null when it was not compiled. */
private Renderer compiled;

/**
* @param currentDepth Indentation depth associated to this block.
*/
Expand All @@ -69,6 +74,25 @@ protected HtmlContinuation(
this.next = next;
}

/**
* Compiles the chain starting in this node. We call it from the preprocessing, before any
* visitor is bound to the chain.
*/
public final void compile() {
compiled = ChainCompiler.compile(this);
}

/**
* Creates a copy of this HtmlContinuation with a new visitor, which emits HTML through the
* generated bytecode whenever the chain was compiled. If a thread does not observe the
* compiled field, then it uses the linked chain, which produces the same HTML.
*/
public final HtmlContinuation compiledCopy(HtmlVisitor visitor) {
return compiled == null
? copy(visitor)
: new HtmlContinuationSyncCompiled(compiled, this, visitor);
}

public HtmlContinuation getNext() {
return next;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* MIT License
*
* Copyright (c) 2025, xmlet HtmlFlow
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package htmlflow.continuations;

import htmlflow.continuations.codegen.ChainCompiler.Renderer;
import htmlflow.visitor.HtmlVisitor;

/**
* HtmlContinuation for a sequence of static blocks and value slots that we compiled to bytecode.
* The generated code appends straight to a StringBuilder, so when the output is some other
* Appendable we fall back to the linked chain.
*
* @author Bernardo Pereira
*/
final class HtmlContinuationSyncCompiled extends HtmlContinuation {

private final Renderer renderer;

private final HtmlContinuation template;

/** The equivalent linked chain, which we only create when some render needs it. */
private HtmlContinuation chain;

HtmlContinuationSyncCompiled(
Renderer renderer,
HtmlContinuation template,
HtmlVisitor visitor
) {
super(-1, false, visitor, null);
this.renderer = renderer;
this.template = template;
}

/**
* We do not synchronize here because concurrent calls just produce equal chains, and all the
* fields of an HtmlContinuation are final.
*/
private HtmlContinuation chain() {
HtmlContinuation built = chain;
if (built == null) chain = built = template.copy(visitor);
return built;
}

/**
* We override execute rather than emitHtml because a compiled chain has no indentation to
* restore and no next node.
*/
@Override
public void execute(Object model) {
Appendable out = visitor.out();
if (out instanceof StringBuilder) {
renderer.render(visitor, (StringBuilder) out, model);
} else {
chain().execute(model);
}
}

@Override
public HtmlContinuation copy(HtmlVisitor v) {
return new HtmlContinuationSyncCompiled(renderer, template, v);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* MIT License
*
* Copyright (c) 2025, xmlet HtmlFlow
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package htmlflow.continuations;

import htmlflow.visitor.HtmlVisitor;
import java.util.function.Function;

/**
* HtmlContinuation for a loop body, which we emit once for each item. The body includes its own
* leading indentation.
*
* @author Bernardo Pereira
*/
public class HtmlContinuationSyncForEach<M, E> extends HtmlContinuationSync {

private final Function<M, ? extends Iterable<E>> items;

private final HtmlContinuation item;

public HtmlContinuationSyncForEach(
Function<M, ? extends Iterable<E>> items,
HtmlContinuation item,
HtmlVisitor visitor,
HtmlContinuation next
) {
super(-1, false, visitor, next);
this.items = items;
this.item = item;
}

@SuppressWarnings("unchecked")
@Override
protected final void emitHtml(Object model) {
for (E e : items.apply((M) model)) item.execute(e);
}

@Override
public HtmlContinuation copy(HtmlVisitor v) {
return new HtmlContinuationSyncForEach<>(
items,
item.copy(v),
v,
next != null ? next.copy(v) : null
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
* MIT License
*
* Copyright (c) 2025, xmlet HtmlFlow
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package htmlflow.continuations;

import htmlflow.visitor.HtmlVisitor;
import htmlflow.visitor.Tags;
import htmlflow.visitor.escape.HtmlEscapers;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.ToDoubleFunction;
import java.util.function.ToIntFunction;
import java.util.function.ToLongFunction;

/**
* HtmlContinuation for a single value read from the model. The markup around the value stays in
* the neighbor static HTML blocks, which are preencoded.
*
* @author Bernardo Pereira
*/
public class HtmlContinuationSyncValue extends HtmlContinuationSync {

public enum Kind {
/** The value as it is, without escaping. */
RAW,
/** The value with HTML escaping. */
TEXT,
INT,
LONG,
DOUBLE,
BOOLEAN,
/** An attribute that we only write when the accessor does not return null. */
ATTR_NULLABLE,
}

private final Kind kind;
private final Object accessor;

/** The name of the attribute, which we only use for the Kind ATTR_NULLABLE. */
private final String attributeName;

/**
* Sets indentation to -1 to inform that visitor should continue with previous indentation.
* A value slot changes neither the depth nor the isClosed of the visitor.
*/
public HtmlContinuationSyncValue(
Kind kind,
Object accessor,
String attributeName,
HtmlVisitor visitor,
HtmlContinuation next
) {
super(-1, false, visitor, next);
this.kind = kind;
this.accessor = accessor;
this.attributeName = attributeName;
}

public Kind kind() {
return kind;
}

public Object accessor() {
return accessor;
}

public String attributeName() {
return attributeName;
}

/**
* Escapes the value of a textOf slot.
*
* @apiNote The generated bytecode invokes this method by its name, so renaming it makes
* every compiled chain fall back to the linked one.
*/
public static String text(Object value) {
return HtmlEscapers.htmlEscaper().escape(String.valueOf(value));
}

/**
* Writes an attribute of an attrOfNullable slot, only when the value is not null.
*
* @apiNote The generated bytecode invokes this method by its name, as in {@link #text}.
*/
public static void attrNullable(
HtmlVisitor visitor,
String name,
Object value
) {
if (value != null) Tags.addAttribute(
visitor.out(),
name,
String.valueOf(value)
);
}

@SuppressWarnings("unchecked")
@Override
protected final void emitHtml(Object model) {
if (kind == Kind.ATTR_NULLABLE) {
attrNullable(
visitor,
attributeName,
((Function<Object, ?>) accessor).apply(model)
);
} else {
visitor.write(rendered(model));
}
}

@SuppressWarnings("unchecked")
private String rendered(Object model) {
switch (kind) {
case INT:
return Integer.toString(
((ToIntFunction<Object>) accessor).applyAsInt(model)
);
case LONG:
return Long.toString(
((ToLongFunction<Object>) accessor).applyAsLong(model)
);
case DOUBLE:
return Double.toString(
((ToDoubleFunction<Object>) accessor).applyAsDouble(model)
);
case BOOLEAN:
return Boolean.toString(
((Predicate<Object>) accessor).test(model)
);
case TEXT:
return text(((Function<Object, ?>) accessor).apply(model));
default:
return String.valueOf(
((Function<Object, ?>) accessor).apply(model)
);
}
}

@Override
public HtmlContinuation copy(HtmlVisitor v) {
return new HtmlContinuationSyncValue(
kind,
accessor,
attributeName,
v,
next != null ? next.copy(v) : null
);
}
}
Loading
Loading