@accordproject/template-engine 2.8.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -1
- package/dist/index.js +50 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +50 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -20,6 +20,31 @@ At a high-level the template engine converts a TemplateMark DOM to an AgreementM
|
|
|
20
20
|
|
|
21
21
|

|
|
22
22
|
|
|
23
|
+
## Execution Pipeline Overview
|
|
24
|
+
|
|
25
|
+
Internally, the Template Engine evaluates templates through a structured execution pipeline:
|
|
26
|
+
|
|
27
|
+
1. **Type Validation**
|
|
28
|
+
The TemplateMark JSON document and the incoming agreement data are validated against their respective Concerto models to ensure structural and type correctness.
|
|
29
|
+
|
|
30
|
+
2. **TypeScript Compilation**
|
|
31
|
+
Embedded TypeScript expressions (such as conditionals, clauses, and formulae) are compiled into JavaScript using the `TemplateMarkToJavaScriptCompiler`.
|
|
32
|
+
|
|
33
|
+
3. **User Code Evaluation**
|
|
34
|
+
During agreement generation, compiled JavaScript expressions are evaluated using the `JavaScriptEvaluator`.
|
|
35
|
+
The evaluator supports two execution strategies:
|
|
36
|
+
- `evalDangerously()` — Executes JavaScript directly within the current process.
|
|
37
|
+
This should only be used with trusted code or in a sandboxed environment (e.g., browser).
|
|
38
|
+
- `evalChildProcess()` — Executes JavaScript in an isolated Node.js child process for improved safety and isolation and is recommended for untrusted template content on the server.
|
|
39
|
+
|
|
40
|
+
4. **AgreementMark Generation**
|
|
41
|
+
The TemplateMark document is traversed, evaluated values are inserted into the document structure, and an AgreementMark JSON document is produced.
|
|
42
|
+
|
|
43
|
+
5. **Output Validation**
|
|
44
|
+
The generated AgreementMark document is validated before being returned.
|
|
45
|
+
|
|
46
|
+
This layered architecture ensures type-safety, deterministic execution of template logic, and isolation during runtime evaluation.
|
|
47
|
+
|
|
23
48
|
## Hello World Template
|
|
24
49
|
|
|
25
50
|
Let's create the simplest template imaginable, the infamous "hello world"!
|
|
@@ -102,7 +127,7 @@ This AgreementMark JSON document can then be passed to the `@accordproject/markd
|
|
|
102
127
|
|
|
103
128
|
The Hello World example just scratches the surface of what can be accomplished! TemplateMark can define optional sections, conditional sections, TypeScript formulae/calculations and even reference external data.
|
|
104
129
|
|
|
105
|
-
Refer to the [full](https://github.com/accordproject/template-engine/tree/main/test/templates/full) example for details.
|
|
130
|
+
Refer to the [full](https://github.com/accordproject/template-engine/tree/main/test/templates/good/full) example for details.
|
|
106
131
|
|
|
107
132
|
> More detailed syntax documentation is to come!
|
|
108
133
|
Read the existing documentation at: https://docs.accordproject.org/docs/markup-templatemark.html
|
package/dist/index.js
CHANGED
|
@@ -122,6 +122,16 @@ function integerDrafter(value, format) {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
function isDuration(value) {
|
|
126
|
+
return value != null && typeof value === "object" && "amount" in value && "unit" in value && typeof value.amount === "number" && typeof value.unit === "string";
|
|
127
|
+
}
|
|
128
|
+
function durationDrafter(value) {
|
|
129
|
+
if (!isDuration(value)) {
|
|
130
|
+
return "0 unknown";
|
|
131
|
+
}
|
|
132
|
+
return `${value.amount} ${value.unit}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
125
135
|
function longDrafter(value, format) {
|
|
126
136
|
if (format) {
|
|
127
137
|
return draftIntegerFormat(value, format);
|
|
@@ -210,6 +220,10 @@ function getDrafter(typeName) {
|
|
|
210
220
|
return longDrafter;
|
|
211
221
|
case "org.accordproject.money@0.3.0.MonetaryAmount":
|
|
212
222
|
return monetaryAmountDrafter;
|
|
223
|
+
case "org.accordproject.time@0.3.0.Duration":
|
|
224
|
+
return durationDrafter;
|
|
225
|
+
case "org.accordproject.time@0.3.0.Period":
|
|
226
|
+
return durationDrafter;
|
|
213
227
|
case "String":
|
|
214
228
|
return stringDrafter;
|
|
215
229
|
default:
|
|
@@ -978,10 +992,45 @@ class TemplateMarkInterpreter {
|
|
|
978
992
|
const serializer = new concertoCore.Serializer(factory, modelManager);
|
|
979
993
|
try {
|
|
980
994
|
serializer.fromJSON(templateMark);
|
|
981
|
-
return templateMark;
|
|
982
995
|
} catch (err) {
|
|
983
996
|
throw new Error(`Generated invalid agreement: ${err}: ${JSON.stringify(templateMark, null, 2)}`);
|
|
984
997
|
}
|
|
998
|
+
const errors = [];
|
|
999
|
+
const templateClass = this.templateClass;
|
|
1000
|
+
const guardBlockPaths = /* @__PURE__ */ new Map();
|
|
1001
|
+
traverse(templateMark).forEach(function(node) {
|
|
1002
|
+
if (!node || typeof node !== "object" || !node.$class) return;
|
|
1003
|
+
const currentPath = this.path.join("/");
|
|
1004
|
+
if (OPTIONAL_DEFINITION_RE.test(node.$class) || CONDITIONAL_DEFINITION_RE.test(node.$class) || WITH_DEFINITION_RE.test(node.$class)) {
|
|
1005
|
+
guardBlockPaths.set(node.name, currentPath);
|
|
1006
|
+
}
|
|
1007
|
+
if (VARIABLE_DEFINITION_RE.test(node.$class) || ENUM_VARIABLE_DEFINITION_RE.test(node.$class) || FORMATTED_VARIABLE_DEFINITION_RE.test(node.$class)) {
|
|
1008
|
+
const propName = node.name;
|
|
1009
|
+
if (propName && propName !== "this") {
|
|
1010
|
+
try {
|
|
1011
|
+
const property = templateClass.getProperty(propName);
|
|
1012
|
+
if (property && property.isOptional()) {
|
|
1013
|
+
const guardPath = guardBlockPaths.get(propName);
|
|
1014
|
+
const isGuarded = guardPath !== void 0 && (currentPath === guardPath || currentPath.startsWith(guardPath + "/"));
|
|
1015
|
+
if (!isGuarded) {
|
|
1016
|
+
errors.push({
|
|
1017
|
+
propertyName: propName,
|
|
1018
|
+
message: `Optional property '${propName}' is used without a guard. Wrap it in {{#optional ${propName}}}...{{/optional}} or {{#if ${propName}}}...{{/if}}.`
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
} catch {
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
if (errors.length > 0) {
|
|
1028
|
+
const errorMessage = `Optional properties used without guards: ${errors.map((e) => e.propertyName).join(", ")}`;
|
|
1029
|
+
const error = new Error(errorMessage);
|
|
1030
|
+
error.errors = errors;
|
|
1031
|
+
throw error;
|
|
1032
|
+
}
|
|
1033
|
+
return templateMark;
|
|
985
1034
|
}
|
|
986
1035
|
/**
|
|
987
1036
|
* Compiles the code nodes containing TS to code nodes containing JS.
|