Skip to content

Commit 2905e16

Browse files
feat: Enhance template rendering and function library with new features and improvements
1 parent fc74388 commit 2905e16

File tree

7 files changed

+223
-44
lines changed

7 files changed

+223
-44
lines changed

examples/initial.jhp

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
11
{% include 'header.jhp' %}
22

3-
<h2>Hello {{ user.name }}! </h2>
3+
<h2>Hello {{ touppercase(user.gender) }} - {{ user.name }}! </h2>
44
<p>
5-
{% if (user.age >= 100) %}
6-
Damn! You're still kickin, huh?
7-
You are a really strong
8-
{% if (user.gender == "M") %}
9-
Man
5+
{% if (user.age >= 100) %}
6+
Damn! You're still kickin, huh?
7+
You are a really strong
8+
{% if (user.gender == "m") %}
9+
Man
10+
{% else %}
11+
Woman
12+
{% endif %}
13+
if I must say....!
14+
{% elseif (user.age >= 10 + 8) %}
15+
Adult? {{ 3 + 27 * 7 / (1 + 4) }}
1016
{% else %}
11-
Woman
17+
Minor
1218
{% endif %}
13-
if I must say....!
14-
{% elseif (user.age >= 10 + 8) %}
15-
Adult? {{ 3 + 27 * 7 / (1 + 4) }}
16-
{% else %}
17-
Minor
18-
{% endif %}
1919
</p>
2020
<ul>
21-
{% for (i = 0; i < 10; i++) %}
22-
<li>{{ i + 1 }}. Item no. {{ i }}!
23-
{% endfor %}
21+
{% for (i = 0; i < 10; i++) %}
22+
<li>{{ i + 1 }}. Item no. {{ i }}!
23+
{% endfor %}
2424
</ul>
2525
<p>
26-
Escaped: {{ '<b>bold</b>' }}
27-
Raw: {{{ '<b>bold</b>' }}}
26+
Escaped: {{ '<b>bold</b>' }}
27+
Raw: {{{ '<b>bold</b>' }}}
2828
</p>
2929

3030
{% include 'footer.jhp' %}

src/main/java/com/hindbiswas/jhp/App.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
import com.hindbiswas.jhp.ast.AstPrettyPrinter;
88
import com.hindbiswas.jhp.ast.AstRendererOld;
99
import com.hindbiswas.jhp.ast.TemplateNode;
10+
import com.hindbiswas.jhp.engine.FunctionLibrary;
11+
import com.hindbiswas.jhp.engine.JhpEngine;
12+
import com.hindbiswas.jhp.engine.Settings;
1013

1114
import java.nio.file.Files;
1215
import java.nio.file.Path;
@@ -16,7 +19,26 @@
1619

1720
public class App {
1821
public static void main(String[] args) throws Exception {
19-
ParseTree tree = generateTree("/home/shinigami/Java/java-hypertext-preprocessor/examples/initial.jhp");
22+
String file = "/home/shinigami/Java/java-hypertext-preprocessor/examples/initial.jhp";
23+
24+
Settings settings = Settings.builder().base("/home/shinigami/Java/java-hypertext-preprocessor/examples/").build();
25+
FunctionLibrary lib = new FunctionLibrary();
26+
JhpEngine engine = new JhpEngine(settings, lib);
27+
28+
Map<String, Object> ctx = new HashMap<>();
29+
Map<String, Object> user = new HashMap<>();
30+
user.put("name", "Alice");
31+
user.put("age", 150);
32+
user.put("gender", "f");
33+
ctx.put("user", user);
34+
ctx.put("title", "Test JHP");
35+
36+
String out = engine.render(file, ctx);
37+
System.out.println(out);
38+
}
39+
40+
public static void oldReneder(String file) throws Exception {
41+
ParseTree tree = generateTree(file);
2042
AstBuilder builder = new AstBuilder();
2143
TemplateNode ast = (TemplateNode) builder.visit(tree);
2244
AstPrettyPrinter.print(ast);
@@ -34,7 +56,6 @@ public static void main(String[] args) throws Exception {
3456
AstRendererOld renderer = new AstRendererOld(Path.of("examples")); // optional base dir
3557
String out = renderer.render(ast, ctx);
3658
System.out.println(out);
37-
3859
}
3960

4061
public static ParseTree generateTree(String file) throws Exception {

src/main/java/com/hindbiswas/jhp/ast/AstRenderer.java

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import java.nio.file.Path;
66
import java.util.*;
77

8-
import com.hindbiswas.jhp.engine.FunctionLibrary;
8+
import com.hindbiswas.jhp.engine.FunctionLibraryContext;
99
import com.hindbiswas.jhp.engine.IssueType;
1010
import com.hindbiswas.jhp.engine.PathResolver;
1111
import com.hindbiswas.jhp.engine.PathToAstParser;
@@ -18,13 +18,13 @@ public class AstRenderer {
1818
private final Settings settings;
1919
private final PathToAstParser parser;
2020
private final PathResolver pathResolver;
21-
private final FunctionLibrary functions;
21+
private final FunctionLibraryContext functions;
2222
private final RuntimeIssueResolver issueResolver;
2323

2424
// per-thread include stack for cycle detection
2525
private final ThreadLocal<Deque<Path>> includeStack = ThreadLocal.withInitial(ArrayDeque::new);
2626

27-
public AstRenderer(Settings settings, FunctionLibrary functions, RuntimeIssueResolver issueResolver,
27+
public AstRenderer(Settings settings, FunctionLibraryContext functions, RuntimeIssueResolver issueResolver,
2828
PathResolver pathResolver, PathToAstParser parser) {
2929
this.settings = settings;
3030
this.functions = functions;
@@ -104,13 +104,15 @@ private void renderInclude(IncludeNode node, Deque<Map<String, Object>> scopes,
104104
issueResolver.handle(IssueType.MISSING_INCLUDE, e.getMessage(), sb, includeStack);
105105
return;
106106
} catch (Exception e) {
107-
issueResolver.handle(IssueType.INCLUDE_ERROR, "Something went wrong trying to resolve the include path: " + node.path + ".", sb, includeStack);
107+
issueResolver.handle(IssueType.INCLUDE_ERROR,
108+
"Something went wrong trying to resolve the include path: " + node.path + ".", sb, includeStack);
108109
return;
109110
}
110111

111112
// Check max depth
112113
if (stack.size() >= settings.maxIncludeDepth) {
113-
issueResolver.handle(IssueType.INCLUDE_MAX_DEPTH, "Max include depth reached: " + settings.maxIncludeDepth, sb, includeStack);
114+
issueResolver.handle(IssueType.INCLUDE_MAX_DEPTH, "Max include depth reached: " + settings.maxIncludeDepth,
115+
sb, includeStack);
114116
return;
115117
}
116118

@@ -138,7 +140,8 @@ private void renderInclude(IncludeNode node, Deque<Map<String, Object>> scopes,
138140
}
139141

140142
} catch (Exception ex) {
141-
issueResolver.handle(IssueType.INCLUDE_ERROR, "Something went wrong trying to parse the include: " + node.path + ".", sb, includeStack);
143+
issueResolver.handle(IssueType.INCLUDE_ERROR,
144+
"Something went wrong trying to parse the include: " + node.path + ".", sb, includeStack);
142145
}
143146
}
144147

@@ -320,12 +323,41 @@ private Object evalExpression(ExpressionNode expr, Deque<Map<String, Object>> sc
320323
}
321324
if (expr instanceof FunctionCallNode) {
322325
FunctionCallNode f = (FunctionCallNode) expr;
323-
Object callee = evalExpression(f.callee, scopes);
326+
327+
// Determine function name (prefer raw identifier names)
328+
String fnName = null;
329+
if (f.callee instanceof IdentifierNode id) {
330+
fnName = id.name; // function name literal
331+
} else {
332+
Object calleeVal = evalExpression(f.callee, scopes);
333+
if (calleeVal instanceof String)
334+
fnName = (String) calleeVal;
335+
}
336+
337+
// build args
324338
List<Object> args = new ArrayList<>();
325339
for (ExpressionNode e : f.args)
326340
args.add(evalExpression(e, scopes));
327-
return callFunction(callee, args, scopes);
341+
342+
if (fnName != null) {
343+
try {
344+
return functions.callFunction(fnName, args, scopes);
345+
} catch (Exception ex) {
346+
StringBuilder tmp = new StringBuilder();
347+
issueResolver.handle(IssueType.FUNCTION_ERROR,
348+
"Function '" + fnName + "' threw: " + ex.getClass().getSimpleName() + ": "
349+
+ ex.getMessage(),
350+
tmp, includeStack);
351+
return tmp.toString();
352+
}
353+
} else {
354+
StringBuilder tmp = new StringBuilder();
355+
issueResolver.handle(IssueType.FUNCTION_CALL_ERROR,
356+
"Invalid function call: " + String.valueOf(f.callee), tmp, includeStack);
357+
return tmp.toString();
358+
}
328359
}
360+
329361
if (expr instanceof UnaryOpNode) {
330362
UnaryOpNode u = (UnaryOpNode) expr;
331363
Object v = evalExpression(u.expr, scopes);
Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,119 @@
11
package com.hindbiswas.jhp.engine;
22

3-
import java.util.List;
4-
import java.util.Deque;
5-
import java.util.Map;
3+
import java.time.Instant;
4+
import java.util.*;
5+
import java.util.concurrent.ConcurrentHashMap;
66

7-
public interface FunctionLibrary {
8-
Object callFunction(String name, List<Object> args, Deque<Map<String, Object>> scopes);
7+
public class FunctionLibrary implements FunctionLibraryContext {
8+
9+
@FunctionalInterface
10+
public interface JhpFunction {
11+
Object apply(List<Object> args, Deque<Map<String, Object>> scopes);
12+
}
13+
14+
private final Map<String, JhpFunction> stdLib;
15+
private final Map<String, JhpFunction> userLib;
16+
17+
public FunctionLibrary() {
18+
this.userLib = new ConcurrentHashMap<>();
19+
Map<String, JhpFunction> builtins = new HashMap<>();
20+
21+
// now() -> returns Instant.now()
22+
builtins.put("now", (args, scopes) -> {
23+
if (args != null && !args.isEmpty()) {
24+
// ignore args but be forgiving
25+
}
26+
return Instant.now();
27+
});
28+
29+
// toUppercase(s)
30+
builtins.put("touppercase", (args, scopes) -> {
31+
if (args == null || args.isEmpty() || args.get(0) == null) return null;
32+
return args.get(0).toString().toUpperCase();
33+
});
34+
35+
// toLowerCase(s)
36+
builtins.put("tolowercase", (args, scopes) -> {
37+
if (args == null || args.isEmpty() || args.get(0) == null) return null;
38+
return args.get(0).toString().toLowerCase();
39+
});
40+
41+
// trim(s)
42+
builtins.put("trim", (args, scopes) -> {
43+
if (args == null || args.isEmpty() || args.get(0) == null) return null;
44+
return args.get(0).toString().trim();
45+
});
46+
47+
// Example: len(s|collection|map)
48+
builtins.put("len", (args, scopes) -> {
49+
if (args == null || args.isEmpty() || args.get(0) == null) return 0;
50+
Object o = args.get(0);
51+
if (o instanceof CharSequence) return ((CharSequence) o).length();
52+
if (o instanceof Collection) return ((Collection<?>) o).size();
53+
if (o instanceof Map) return ((Map<?, ?>) o).size();
54+
if (o.getClass().isArray()) return java.lang.reflect.Array.getLength(o);
55+
return 0;
56+
});
57+
58+
this.stdLib = Collections.unmodifiableMap(builtins);
59+
}
60+
61+
public JhpFunction register(String name, JhpFunction fn) throws IllegalArgumentException {
62+
validateNameAndFn(name, fn);
63+
return userLib.put(normalize(name), fn);
64+
}
65+
66+
public JhpFunction unregister(String name) {
67+
if (name == null) return null;
68+
return userLib.remove(normalize(name));
69+
}
70+
71+
public boolean hasFunction(String name) {
72+
if (name == null) return false;
73+
String n = normalize(name);
74+
return userLib.containsKey(n) || stdLib.containsKey(n);
75+
}
76+
77+
@Override
78+
public Object callFunction(String name, List<Object> args, Deque<Map<String, Object>> scopes) {
79+
if (name == null) throw new IllegalArgumentException("Function name cannot be null");
80+
String n = normalize(name);
81+
82+
// priority: userLib -> stdLib
83+
JhpFunction fn = userLib.get(n);
84+
if (fn != null) {
85+
return safeInvoke(fn, args, scopes, n);
86+
}
87+
88+
fn = stdLib.get(n);
89+
if (fn != null) {
90+
return safeInvoke(fn, args, scopes, n);
91+
}
92+
93+
throw new IllegalArgumentException("Function not found: " + name);
94+
}
95+
96+
/* ----------------- helpers ----------------- */
97+
98+
private Object safeInvoke(JhpFunction fn, List<Object> args, Deque<Map<String, Object>> scopes, String name) {
99+
try {
100+
return fn.apply(args == null ? Collections.emptyList() : args, scopes);
101+
} catch (RuntimeException re) {
102+
// wrap to provide contextual info
103+
throw new RuntimeException("Error executing function '" + name + "': " + re.getMessage(), re);
104+
}
105+
}
106+
107+
private void validateNameAndFn(String name, JhpFunction fn) throws IllegalArgumentException {
108+
if (name == null || name.trim().isEmpty()) {
109+
throw new IllegalArgumentException("Function name cannot be null/empty");
110+
}
111+
if (fn == null) {
112+
throw new IllegalArgumentException("Function implementation cannot be null");
113+
}
114+
}
115+
116+
private String normalize(String name) {
117+
return name.trim().toLowerCase(Locale.ROOT);
118+
}
9119
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.hindbiswas.jhp.engine;
2+
3+
import java.util.List;
4+
import java.util.Deque;
5+
import java.util.Map;
6+
7+
public interface FunctionLibraryContext {
8+
Object callFunction(String name, List<Object> args, Deque<Map<String, Object>> scopes);
9+
}

src/main/java/com/hindbiswas/jhp/engine/JhpEngine.java

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,12 @@ private final class IssueResolver implements RuntimeIssueResolver {
9797
public void handle(IssueType type, String message, StringBuilder sb, ThreadLocal<Deque<Path>> includeStack) {
9898
if (settings.issueHandleMode == IssueHandleMode.COMMENT) {
9999
sb.append("<!-- ").append(type).append(": ").append(message).append(" -->\n");
100-
} else if (settings.issueHandleMode == IssueHandleMode.THROW) {
100+
return;
101+
}
102+
if (settings.issueHandleMode == IssueHandleMode.THROW) {
101103
throw new RuntimeException(type + ": " + message);
102-
} else if (settings.issueHandleMode == IssueHandleMode.DEBUG) {
104+
}
105+
if (settings.issueHandleMode == IssueHandleMode.DEBUG) {
103106
// Build debug info
104107
String ts = java.time.Instant.now().toString();
105108
String thread = Thread.currentThread().getName();
@@ -166,9 +169,8 @@ public void handle(IssueType type, String message, StringBuilder sb, ThreadLocal
166169
sb.append("</details>\n");
167170

168171
sb.append("<!-- ISSUE DEBUG END -->\n");
169-
} else if (settings.issueHandleMode == IssueHandleMode.IGNORE) {
170-
// intentionally do nothing
171-
}
172+
}
173+
172174
}
173175

174176
// small helper to append a table row (keeps HTML escaped where appropriate)
@@ -205,21 +207,26 @@ private String escapeHtml(String s) {
205207

206208
private final AstParser parser = new AstParser();
207209
private final Settings settings;
208-
private final FunctionLibrary functionLibrary;
210+
private final FunctionLibraryContext functionLibrary;
209211
private final RuntimeIssueResolver issueResolver = new IssueResolver();
212+
private final IncludePathResolver includePathResolver = new IncludePathResolver();
210213

211214
// AST cache
212215
private final Map<Path, TemplateNode> astCache = new HashMap<>();
213216

214-
public JhpEngine(Settings settings, FunctionLibrary functionLibrary) {
217+
public JhpEngine(Settings settings, FunctionLibraryContext functionLibrary) {
215218
this.settings = settings;
216219
this.functionLibrary = functionLibrary;
217220
}
218221

219222
public String render(Path templatePath, Map<String, Object> context) throws Exception {
220223
TemplateNode ast = parser.parse(templatePath);
221-
AstRenderer renderer = new AstRenderer(settings, functionLibrary, issueResolver, new IncludePathResolver(),
222-
parser);
224+
AstRenderer renderer = new AstRenderer(settings, functionLibrary, issueResolver, includePathResolver, parser);
223225
return renderer.render(ast, context);
224226
}
227+
228+
public String render(String pathTxt, Map<String, Object> context) throws Exception {
229+
Path path = includePathResolver.resolve(pathTxt, null);
230+
return render(path, context);
231+
}
225232
}

src/main/java/com/hindbiswas/jhp/engine/Settings.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ public static final class SettingsBuilder {
99
private int maxIncludeDepth = defaultMaxIncludeDepth;
1010
private IssueHandleMode issueHandleMode = defaultIssueHandleMode;
1111

12-
public SettingsBuilder base(Path base) {
13-
this.base = base;
12+
public SettingsBuilder base(String base) {
13+
this.base = Path.of(base);
1414
return this;
1515
}
1616

0 commit comments

Comments
 (0)