@asterdoc/render 0.0.0-stage → 0.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/LICENSE +25 -0
- package/README.md +264 -2
- package/dist/check-BA5xdYgi.d.ts +69 -0
- package/dist/chunk-4QVR6LUY.js +98 -0
- package/dist/chunk-ICQLU4FL.js +630 -0
- package/dist/chunk-PNYKTSIT.js +1267 -0
- package/dist/chunk-U7LF3GN4.js +34 -0
- package/dist/context-BmjDcc_j.d.ts +32 -0
- package/dist/docx.d.ts +37 -0
- package/dist/docx.js +38 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +69 -0
- package/dist/layer-bindings-CFXyN7SM.d.ts +94 -0
- package/dist/pdf.d.ts +620 -0
- package/dist/pdf.js +4638 -0
- package/dist/signatures-DPaL4gay.d.ts +20 -0
- package/dist/signatures-cS3V0yR3.d.ts +27 -0
- package/dist/text/field-formatter.d.ts +46 -0
- package/dist/text/field-formatter.js +21 -0
- package/dist/text.d.ts +44 -0
- package/dist/text.js +77 -0
- package/package.json +103 -3
|
@@ -0,0 +1,1267 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RENDER_DATA_ROOTS
|
|
3
|
+
} from "./chunk-U7LF3GN4.js";
|
|
4
|
+
import {
|
|
5
|
+
FormattedFieldValue,
|
|
6
|
+
formatFieldData,
|
|
7
|
+
unwrapFormattedValue
|
|
8
|
+
} from "./chunk-ICQLU4FL.js";
|
|
9
|
+
|
|
10
|
+
// src/template/errors.ts
|
|
11
|
+
function describeLocation(diagnostic) {
|
|
12
|
+
const parts = [];
|
|
13
|
+
if (diagnostic.location) parts.push(diagnostic.location);
|
|
14
|
+
if (diagnostic.position) parts.push(`line ${diagnostic.position.line}, column ${diagnostic.position.column}`);
|
|
15
|
+
return parts.join(", ");
|
|
16
|
+
}
|
|
17
|
+
var TemplateError = class _TemplateError extends Error {
|
|
18
|
+
code;
|
|
19
|
+
layer;
|
|
20
|
+
expression;
|
|
21
|
+
position;
|
|
22
|
+
location;
|
|
23
|
+
diagnostic;
|
|
24
|
+
constructor(diagnostic, layer) {
|
|
25
|
+
const where = [layer ? `layer "${layer}"` : "", describeLocation(diagnostic)].filter(Boolean).join(", ");
|
|
26
|
+
const expression = diagnostic.expression === void 0 ? "" : ` in {{${diagnostic.expression}}}`;
|
|
27
|
+
super(`${where ? `Template error at ${where}` : "Template error"}${expression}: ${diagnostic.message}`);
|
|
28
|
+
this.name = "TemplateError";
|
|
29
|
+
this.code = diagnostic.code;
|
|
30
|
+
this.layer = layer;
|
|
31
|
+
this.expression = diagnostic.expression;
|
|
32
|
+
this.position = diagnostic.position;
|
|
33
|
+
this.location = diagnostic.location;
|
|
34
|
+
this.diagnostic = diagnostic;
|
|
35
|
+
}
|
|
36
|
+
/** The same error, naming the layer it came from. */
|
|
37
|
+
inLayer(layer) {
|
|
38
|
+
return layer && !this.layer ? new _TemplateError(this.diagnostic, layer) : this;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var COMPARISON_HELPERS = { eq: "==", ne: "!=", gt: ">", gte: ">=", lt: "<", lte: "<=" };
|
|
42
|
+
function removedSyntaxHint(source) {
|
|
43
|
+
const text = source.trim();
|
|
44
|
+
const helper = text.match(/^\(?\s*(eq|ne|gt|gte|lt|lte|not|and|or|contains|default)\s+(?![=<>!])/);
|
|
45
|
+
if (helper) {
|
|
46
|
+
const name = helper[1];
|
|
47
|
+
if (COMPARISON_HELPERS[name]) {
|
|
48
|
+
return `The "${name}" helper is removed; compare with the ${COMPARISON_HELPERS[name]} operator, such as fields.status ${COMPARISON_HELPERS[name]} "active".`;
|
|
49
|
+
}
|
|
50
|
+
if (name === "not") return 'The "not" helper is removed; write not before the condition, such as not fields.approved.';
|
|
51
|
+
if (name === "and" || name === "or") return `The "${name}" helper is removed; join conditions with the ${name} operator, such as fields.a ${name} fields.b.`;
|
|
52
|
+
if (name === "contains") return 'The "contains" helper is removed; write "value" in fields.list or contains(fields.list, "value").';
|
|
53
|
+
return 'The "default" helper is removed; use coalesce(fields.value, "fallback").';
|
|
54
|
+
}
|
|
55
|
+
if (/^(signature|initials|signatureDate|capacity|printedName)\s+["']/.test(text)) {
|
|
56
|
+
const name = text.split(/\s/)[0];
|
|
57
|
+
return `Write ${name}("location") inside a party loop, or ${name}(parties.role, "location").`;
|
|
58
|
+
}
|
|
59
|
+
if (/^INS\s/.test(text)) return "The INS form is removed; write the expression alone, such as {{fields.name}}.";
|
|
60
|
+
if (text.startsWith("&")) return "The {{& }} form is removed; use {{{ }}} to print without HTML escaping.";
|
|
61
|
+
if (/===|!==/.test(text)) return "The === and !== operators are removed; use == and !=.";
|
|
62
|
+
if (/(^|[^\w.])this\b/.test(text)) return '"this" is removed; use item inside a loop, or a full path such as fields.name.';
|
|
63
|
+
if (text.includes("../")) return '"../" is removed; use parent inside a nested loop, or a full path from the root.';
|
|
64
|
+
if (/@root\b/.test(text)) return '"@root" is removed; write the path from the root, such as fields.name.';
|
|
65
|
+
if (/@index\b/.test(text)) return '"@index" is removed; use index(item) inside a loop.';
|
|
66
|
+
if (/@first\b/.test(text)) return '"@first" is removed; use first(item) inside a loop.';
|
|
67
|
+
if (/@last\b/.test(text)) return '"@last" is removed; use last(item) inside a loop.';
|
|
68
|
+
if (/@key\b/.test(text)) return '"@key" is removed; a loop runs over a list.';
|
|
69
|
+
if (/(^|[^\w])\$[A-Za-z_]/.test(text)) return "The $name form is removed; a DOCX loop names its row, such as {{FOR line IN fields.items}} and then {{line.amount}}.";
|
|
70
|
+
if (/\.\[\d+\]/.test(text)) return "The .[n] segment is removed; index a list with [n], such as parties.preparer[0].";
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/docx/render.ts
|
|
75
|
+
import { defaultFormatter } from "@asterdoc/format";
|
|
76
|
+
import { unzipSync, zipSync } from "fflate";
|
|
77
|
+
|
|
78
|
+
// src/template/markers.ts
|
|
79
|
+
import { parse } from "@asterdoc/expr";
|
|
80
|
+
function positionFinder(source, base = { line: 1, column: 1 }) {
|
|
81
|
+
const lineStarts = [0];
|
|
82
|
+
for (let index = 0; index < source.length; index++) if (source[index] === "\n") lineStarts.push(index + 1);
|
|
83
|
+
return (offset) => {
|
|
84
|
+
let line = 0;
|
|
85
|
+
while (line + 1 < lineStarts.length && lineStarts[line + 1] <= offset) line++;
|
|
86
|
+
const column = offset - lineStarts[line] + 1;
|
|
87
|
+
return line === 0 ? { line: base.line, column: base.column + column - 1 } : { line: base.line + line, column };
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function expressionSlot(source, position) {
|
|
91
|
+
const trimmed = source.trim();
|
|
92
|
+
if (trimmed === "") {
|
|
93
|
+
return { source: trimmed, position, problem: { code: "syntax", message: "The marker holds no expression.", expression: trimmed, position } };
|
|
94
|
+
}
|
|
95
|
+
const { ast, errors } = parse(trimmed);
|
|
96
|
+
if (ast) return { source: trimmed, ast, position };
|
|
97
|
+
const hint = removedSyntaxHint(trimmed);
|
|
98
|
+
return {
|
|
99
|
+
source: trimmed,
|
|
100
|
+
position,
|
|
101
|
+
problem: {
|
|
102
|
+
code: hint ? "removed-syntax" : "syntax",
|
|
103
|
+
message: hint ?? errors[0]?.message ?? "The expression does not parse.",
|
|
104
|
+
expression: trimmed,
|
|
105
|
+
position
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
var TAG = /\{\{\{[\s\S]*?\}\}\}|\{\{[\s\S]*?\}\}/g;
|
|
110
|
+
function parseTemplate(template, base) {
|
|
111
|
+
const positionAt = positionFinder(template, base);
|
|
112
|
+
const root = [];
|
|
113
|
+
const stack = [];
|
|
114
|
+
let output = root;
|
|
115
|
+
let cursor = 0;
|
|
116
|
+
let skipLineBreak = false;
|
|
117
|
+
const fail2 = (message, offset) => {
|
|
118
|
+
throw new TemplateError({ code: "markers", message, position: positionAt(offset) });
|
|
119
|
+
};
|
|
120
|
+
const standalone = (start, end) => {
|
|
121
|
+
const lineStart = template.lastIndexOf("\n", start - 1) + 1;
|
|
122
|
+
const lineEndIndex = template.indexOf("\n", end);
|
|
123
|
+
const lineEnd = lineEndIndex === -1 ? template.length : lineEndIndex;
|
|
124
|
+
return /^[\t ]*$/.test(template.slice(lineStart, start)) && /^[\t \r]*$/.test(template.slice(end, lineEnd));
|
|
125
|
+
};
|
|
126
|
+
const pushText = (value) => {
|
|
127
|
+
let text = value;
|
|
128
|
+
if (skipLineBreak) {
|
|
129
|
+
text = text.replace(/^[\t ]*\r?\n?/, "");
|
|
130
|
+
skipLineBreak = false;
|
|
131
|
+
}
|
|
132
|
+
if (text.length > 0) output.push({ type: "text", value: text });
|
|
133
|
+
};
|
|
134
|
+
const trimLineBefore = () => {
|
|
135
|
+
const last = output.at(-1);
|
|
136
|
+
if (last?.type !== "text") return;
|
|
137
|
+
last.value = last.value.replace(/[\t ]*$/, "");
|
|
138
|
+
if (last.value.length === 0) output.pop();
|
|
139
|
+
};
|
|
140
|
+
for (const match of template.matchAll(TAG)) {
|
|
141
|
+
const index = match.index ?? 0;
|
|
142
|
+
const token = match[0];
|
|
143
|
+
const end = index + token.length;
|
|
144
|
+
if (index > cursor) pushText(template.slice(cursor, index));
|
|
145
|
+
else if (skipLineBreak) skipLineBreak = false;
|
|
146
|
+
cursor = end;
|
|
147
|
+
const triple = token.startsWith("{{{");
|
|
148
|
+
const inner = token.slice(triple ? 3 : 2, triple ? -3 : -2);
|
|
149
|
+
const innerOffset = index + (triple ? 3 : 2) + (inner.length - inner.trimStart().length);
|
|
150
|
+
const content = inner.trim();
|
|
151
|
+
if (!triple && content.startsWith("!")) continue;
|
|
152
|
+
const isBlockTag = !triple && (content.startsWith("#") || content.startsWith("/") || content === "else");
|
|
153
|
+
if (isBlockTag && standalone(index, end)) {
|
|
154
|
+
trimLineBefore();
|
|
155
|
+
skipLineBreak = true;
|
|
156
|
+
}
|
|
157
|
+
if (!triple && content.startsWith("#")) {
|
|
158
|
+
const body = content.slice(1);
|
|
159
|
+
const name = body.match(/^\S*/)?.[0] ?? "";
|
|
160
|
+
const expression = body.slice(name.length);
|
|
161
|
+
if (name === "with") fail2("{{#with}} is removed; write full paths, such as parties.tenant.name.", index);
|
|
162
|
+
if (name !== "if" && name !== "unless" && name !== "each") fail2(`Unsupported block {{#${name}}}; use {{#if}}, {{#unless}}, or {{#each}}.`, index);
|
|
163
|
+
const expressionOffset = innerOffset + 1 + name.length + (expression.length - expression.trimStart().length);
|
|
164
|
+
const block = {
|
|
165
|
+
type: name,
|
|
166
|
+
slot: expressionSlot(expression, positionAt(expressionOffset)),
|
|
167
|
+
children: [],
|
|
168
|
+
inverse: []
|
|
169
|
+
};
|
|
170
|
+
output.push(block);
|
|
171
|
+
stack.push({ block, output, tag: name, position: positionAt(index) });
|
|
172
|
+
output = block.children;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (!triple && content === "else") {
|
|
176
|
+
const current = stack.at(-1);
|
|
177
|
+
if (!current) fail2("Unexpected {{else}}", index);
|
|
178
|
+
if (output === current.block.inverse) fail2("A block has more than one {{else}}", index);
|
|
179
|
+
output = current.block.inverse;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (!triple && content.startsWith("/")) {
|
|
183
|
+
const name = content.slice(1).trim();
|
|
184
|
+
const current = stack.pop();
|
|
185
|
+
if (!current || current.tag !== name) fail2(`Unexpected closing block {{/${name}}}`, index);
|
|
186
|
+
output = current.output;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
output.push({ type: "value", slot: expressionSlot(inner, positionAt(innerOffset)), escaped: !triple });
|
|
190
|
+
}
|
|
191
|
+
if (cursor < template.length) pushText(template.slice(cursor));
|
|
192
|
+
const open = stack.at(-1);
|
|
193
|
+
if (open) throw new TemplateError({ code: "markers", message: `Unclosed block {{#${open.tag}}}`, position: open.position });
|
|
194
|
+
return root;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/template/scope.ts
|
|
198
|
+
import {
|
|
199
|
+
buildRegistry,
|
|
200
|
+
EvaluationError,
|
|
201
|
+
evaluate,
|
|
202
|
+
T,
|
|
203
|
+
truthy,
|
|
204
|
+
Values
|
|
205
|
+
} from "@asterdoc/expr";
|
|
206
|
+
var DEFAULT_REGISTRY = buildRegistry();
|
|
207
|
+
var LOOP_FUNCTIONS = [
|
|
208
|
+
{ name: "index", category: "collection", params: [{ name: "row", type: T.unknown }], returns: { kind: "fixed", type: T.number }, hostInjected: true, deterministic: true },
|
|
209
|
+
{ name: "first", category: "collection", params: [{ name: "row", type: T.unknown }], returns: { kind: "fixed", type: T.boolean }, hostInjected: true, deterministic: true },
|
|
210
|
+
{ name: "last", category: "collection", params: [{ name: "row", type: T.unknown }], returns: { kind: "fixed", type: T.boolean }, hostInjected: true, deterministic: true }
|
|
211
|
+
];
|
|
212
|
+
var LOOP_FUNCTION_NAMES = new Set(LOOP_FUNCTIONS.map((signature) => signature.name));
|
|
213
|
+
function withLoopFunctions(base) {
|
|
214
|
+
const registry = base ?? DEFAULT_REGISTRY;
|
|
215
|
+
const signatures = new Map(registry.signatures);
|
|
216
|
+
for (const signature of LOOP_FUNCTIONS) signatures.set(signature.name, signature);
|
|
217
|
+
return {
|
|
218
|
+
signatures,
|
|
219
|
+
has: (name) => signatures.has(name),
|
|
220
|
+
get: (name) => signatures.get(name),
|
|
221
|
+
names: () => [...signatures.keys()]
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function referenceSegments(node) {
|
|
225
|
+
if (node.kind === "Identifier") return [node.name];
|
|
226
|
+
if (node.kind === "Member") {
|
|
227
|
+
const base = referenceSegments(node.object);
|
|
228
|
+
return base ? [...base, node.property] : void 0;
|
|
229
|
+
}
|
|
230
|
+
if (node.kind === "Index" && (node.index.kind === "NumberLiteral" && /^\d+$/.test(node.index.value) || node.index.kind === "StringLiteral")) {
|
|
231
|
+
const base = referenceSegments(node.object);
|
|
232
|
+
return base ? [...base, node.index.value] : void 0;
|
|
233
|
+
}
|
|
234
|
+
return void 0;
|
|
235
|
+
}
|
|
236
|
+
var blocked = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
237
|
+
function dataPath(value, segments) {
|
|
238
|
+
let current = value;
|
|
239
|
+
for (const [index, segment] of segments.entries()) {
|
|
240
|
+
current = unwrapFormattedValue(current);
|
|
241
|
+
if (blocked.has(segment) || current === null || current === void 0) return void 0;
|
|
242
|
+
if (typeof current !== "object") return void 0;
|
|
243
|
+
current = current[segment];
|
|
244
|
+
if (index === segments.length - 1) return current;
|
|
245
|
+
}
|
|
246
|
+
return current;
|
|
247
|
+
}
|
|
248
|
+
function fromValue(value) {
|
|
249
|
+
switch (value.kind) {
|
|
250
|
+
case "number":
|
|
251
|
+
return value.value.toNumber();
|
|
252
|
+
case "string":
|
|
253
|
+
case "boolean":
|
|
254
|
+
return value.value;
|
|
255
|
+
case "null":
|
|
256
|
+
return null;
|
|
257
|
+
case "array":
|
|
258
|
+
return value.value.map(fromValue);
|
|
259
|
+
case "object":
|
|
260
|
+
return Object.fromEntries([...value.value].map(([key, member]) => [key, fromValue(member)]));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function isPlainRecord(value) {
|
|
264
|
+
return typeof value === "object" && value !== null && !(value instanceof FormattedFieldValue);
|
|
265
|
+
}
|
|
266
|
+
function frameFor(frames, name) {
|
|
267
|
+
let rows = 0;
|
|
268
|
+
for (let index = frames.length - 1; index >= 0; index--) {
|
|
269
|
+
const frame = frames[index];
|
|
270
|
+
if (frame.kind === "named") {
|
|
271
|
+
if (frame.name === name) return frame;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
rows++;
|
|
275
|
+
if (name === "item" && rows === 1 || name === "parent" && rows === 2) return frame;
|
|
276
|
+
}
|
|
277
|
+
return void 0;
|
|
278
|
+
}
|
|
279
|
+
var TemplateScope = class _TemplateScope {
|
|
280
|
+
constructor(root, environment, frames = []) {
|
|
281
|
+
this.root = root;
|
|
282
|
+
this.environment = environment;
|
|
283
|
+
this.frames = frames;
|
|
284
|
+
}
|
|
285
|
+
root;
|
|
286
|
+
environment;
|
|
287
|
+
frames;
|
|
288
|
+
push(frame) {
|
|
289
|
+
return new _TemplateScope(this.root, this.environment, [...this.frames, frame]);
|
|
290
|
+
}
|
|
291
|
+
/** The innermost row, whatever binds it. */
|
|
292
|
+
current() {
|
|
293
|
+
return this.frames.at(-1);
|
|
294
|
+
}
|
|
295
|
+
frameFor(name) {
|
|
296
|
+
return frameFor(this.frames, name);
|
|
297
|
+
}
|
|
298
|
+
context() {
|
|
299
|
+
if (this.frames.length === 0) return this.root;
|
|
300
|
+
const rowFunction = (answer) => (args) => {
|
|
301
|
+
const frame = this.frames.find((candidate) => candidate.value === args[0]);
|
|
302
|
+
if (!frame) throw new EvaluationError("type-error", "index, first, and last take a loop row, such as item");
|
|
303
|
+
return answer(frame);
|
|
304
|
+
};
|
|
305
|
+
const rowVisible = this.root.rowVisible;
|
|
306
|
+
return {
|
|
307
|
+
lookup: (name) => this.frameFor(name)?.value ?? this.root.lookup(name),
|
|
308
|
+
asOf: this.root.asOf,
|
|
309
|
+
registry: withLoopFunctions(this.root.registry),
|
|
310
|
+
hostFunctions: {
|
|
311
|
+
...this.root.hostFunctions,
|
|
312
|
+
index: rowFunction((frame) => Values.num(String(frame.index))),
|
|
313
|
+
first: rowFunction((frame) => Values.boolean(frame.index === 0)),
|
|
314
|
+
last: rowFunction((frame) => Values.boolean(frame.index === frame.count - 1))
|
|
315
|
+
},
|
|
316
|
+
rowVisible: rowVisible && ((listPath, indices) => {
|
|
317
|
+
const [head, ...rest] = listPath.split(".");
|
|
318
|
+
const frame = head ? this.frameFor(head) : void 0;
|
|
319
|
+
if (!frame) return rowVisible(listPath, indices);
|
|
320
|
+
if (!frame.origin) return true;
|
|
321
|
+
return rowVisible([frame.origin.listPath, ...rest].join("."), [...frame.origin.indices, ...indices]);
|
|
322
|
+
})
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
evaluate(node, source, position) {
|
|
326
|
+
try {
|
|
327
|
+
return evaluate(node, this.context());
|
|
328
|
+
} catch (error) {
|
|
329
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
330
|
+
const code = error instanceof EvaluationError ? error.code : "evaluation-error";
|
|
331
|
+
throw new TemplateError({ code, message, expression: source, position }, this.environment.layer);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/** The render data an expression reads, when it is a path into it. */
|
|
335
|
+
dataAt(node) {
|
|
336
|
+
const segments = referenceSegments(node);
|
|
337
|
+
if (!segments) return void 0;
|
|
338
|
+
const [head, ...rest] = segments;
|
|
339
|
+
const frame = head ? this.frameFor(head) : void 0;
|
|
340
|
+
if (frame) return rest.length === 0 ? frame.data : dataPath(frame.data, rest);
|
|
341
|
+
return this.environment.resolveData(segments);
|
|
342
|
+
}
|
|
343
|
+
/** Where the rows of a list path sit in the artifact, when the list is a path it declares. */
|
|
344
|
+
originOf(node) {
|
|
345
|
+
const segments = referenceSegments(node);
|
|
346
|
+
if (!segments) return void 0;
|
|
347
|
+
const [head, ...rest] = segments;
|
|
348
|
+
const frame = head ? this.frameFor(head) : void 0;
|
|
349
|
+
if (frame) {
|
|
350
|
+
return frame.origin ? { listPath: [frame.origin.listPath, ...rest].join("."), indices: frame.origin.indices } : void 0;
|
|
351
|
+
}
|
|
352
|
+
return head === "fields" ? { listPath: segments.join("."), indices: [] } : void 0;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* The text a placeholder prints. A path prints the value the way the
|
|
356
|
+
* artifact formats it; a computed value is formatted by its type.
|
|
357
|
+
*/
|
|
358
|
+
present(node, source, position) {
|
|
359
|
+
if (node.kind === "Conditional") {
|
|
360
|
+
const test = this.evaluate(node.test, source, position);
|
|
361
|
+
return this.present(truthy(test) ? node.consequent : node.alternate, source, position);
|
|
362
|
+
}
|
|
363
|
+
if (node.kind === "Call" && node.callee === "coalesce") {
|
|
364
|
+
for (const arg of node.args) {
|
|
365
|
+
if (this.evaluate(arg, source, position).kind !== "null") return this.present(arg, source, position);
|
|
366
|
+
}
|
|
367
|
+
return "";
|
|
368
|
+
}
|
|
369
|
+
if (referenceSegments(node)) {
|
|
370
|
+
const data = this.dataAt(node);
|
|
371
|
+
if (data !== void 0 && !isPlainRecord(data) && !Array.isArray(data)) {
|
|
372
|
+
return data === null ? "" : String(data);
|
|
373
|
+
}
|
|
374
|
+
if (data instanceof FormattedFieldValue) return String(data);
|
|
375
|
+
}
|
|
376
|
+
return this.formatValue(this.evaluate(node, source, position), node);
|
|
377
|
+
}
|
|
378
|
+
formatValue(value, node) {
|
|
379
|
+
const { formatter } = this.environment;
|
|
380
|
+
switch (value.kind) {
|
|
381
|
+
case "null":
|
|
382
|
+
return "";
|
|
383
|
+
case "boolean":
|
|
384
|
+
return formatter.formatBoolean(value.value);
|
|
385
|
+
case "number":
|
|
386
|
+
return formatter.formatNumber(value.value.toNumber());
|
|
387
|
+
case "string": {
|
|
388
|
+
const signature = node.kind === "Call" ? (this.root.registry ?? DEFAULT_REGISTRY).get(node.callee) : void 0;
|
|
389
|
+
const returns = signature?.returns.kind === "fixed" ? signature.returns.type.kind : void 0;
|
|
390
|
+
if (returns === "date") return formatter.formatDate(value.value);
|
|
391
|
+
if (returns === "datetime") return formatter.formatDatetime(value.value);
|
|
392
|
+
return value.value;
|
|
393
|
+
}
|
|
394
|
+
case "array":
|
|
395
|
+
return value.value.map((element) => this.formatValue(element, node)).join(", ");
|
|
396
|
+
case "object": {
|
|
397
|
+
const amount = value.value.get("amount");
|
|
398
|
+
const currency = value.value.get("currency");
|
|
399
|
+
if (amount?.kind === "number" && currency?.kind === "string") {
|
|
400
|
+
return formatter.formatMoney({ amount: amount.value.toNumber(), currency: currency.value });
|
|
401
|
+
}
|
|
402
|
+
return "";
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// src/text/template.ts
|
|
409
|
+
var Markup = class {
|
|
410
|
+
constructor(value) {
|
|
411
|
+
this.value = value;
|
|
412
|
+
}
|
|
413
|
+
value;
|
|
414
|
+
toString() {
|
|
415
|
+
return this.value;
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
var SIGNING_DIRECTIVES = /* @__PURE__ */ new Set(["signature", "initials", "signatureDate", "capacity", "printedName"]);
|
|
419
|
+
function fail(slot, message, code, layer) {
|
|
420
|
+
throw new TemplateError({ code, message, expression: slot.source, position: slot.position }, layer);
|
|
421
|
+
}
|
|
422
|
+
function parsed(slot, layer) {
|
|
423
|
+
if (slot.ast) return slot.ast;
|
|
424
|
+
throw new TemplateError(slot.problem, layer);
|
|
425
|
+
}
|
|
426
|
+
function condition(value, slot, layer) {
|
|
427
|
+
if (value.kind === "boolean") return value.value;
|
|
428
|
+
if (value.kind === "null") return false;
|
|
429
|
+
return fail(slot, `A condition must be boolean, got ${value.kind}.`, "non-boolean-gate", layer);
|
|
430
|
+
}
|
|
431
|
+
function loopRows(value, slot, layer) {
|
|
432
|
+
if (value.kind === "array") return value.value;
|
|
433
|
+
if (value.kind === "null") return [];
|
|
434
|
+
return fail(slot, `A loop source must be a list, got ${value.kind}.`, "type-mismatch", layer);
|
|
435
|
+
}
|
|
436
|
+
function loopFrames(scope, node, rows, bind) {
|
|
437
|
+
const data = unwrapFormattedValue(scope.dataAt(node));
|
|
438
|
+
const origin = scope.originOf(node);
|
|
439
|
+
return rows.map((value, index) => ({
|
|
440
|
+
...bind,
|
|
441
|
+
value,
|
|
442
|
+
data: Array.isArray(data) ? data[index] : void 0,
|
|
443
|
+
index,
|
|
444
|
+
count: rows.length,
|
|
445
|
+
origin: origin ? { listPath: origin.listPath, indices: [...origin.indices, index] } : void 0
|
|
446
|
+
}));
|
|
447
|
+
}
|
|
448
|
+
function renderDirective(node, slot, scope, options) {
|
|
449
|
+
const directive = options.directives?.[node.callee];
|
|
450
|
+
if (!directive) return fail(slot, `${node.callee}() is not available in this layer.`, "unknown-function", options.layer);
|
|
451
|
+
if (node.args.length < 1 || node.args.length > 2) {
|
|
452
|
+
return fail(slot, `${node.callee} takes a location, or a party and a location, such as ${node.callee}(parties.tenant, "tenant-sign").`, "arity", options.layer);
|
|
453
|
+
}
|
|
454
|
+
const locationNode = node.args.at(-1);
|
|
455
|
+
const location = scope.evaluate(locationNode, slot.source, slot.position);
|
|
456
|
+
if (location.kind !== "string") return fail(slot, `${node.callee}'s location must be a string.`, "type-mismatch", options.layer);
|
|
457
|
+
let party;
|
|
458
|
+
if (node.args.length === 2) {
|
|
459
|
+
const partyNode = node.args[0];
|
|
460
|
+
const data = scope.dataAt(partyNode);
|
|
461
|
+
party = data === void 0 ? fromValue(scope.evaluate(partyNode, slot.source, slot.position)) : unwrapFormattedValue(data);
|
|
462
|
+
} else {
|
|
463
|
+
const row = scope.current();
|
|
464
|
+
if (!row) return fail(slot, `${node.callee}("${location.value}") needs a party: use it inside a party loop, or pass the party first, such as ${node.callee}(parties.tenant, "${location.value}").`, "arity", options.layer);
|
|
465
|
+
party = row.data === void 0 ? fromValue(row.value) : unwrapFormattedValue(row.data);
|
|
466
|
+
}
|
|
467
|
+
const result = directive(party, options.root, [location.value]);
|
|
468
|
+
if (result instanceof Markup) return result;
|
|
469
|
+
return result === null || result === void 0 ? "" : String(result);
|
|
470
|
+
}
|
|
471
|
+
function renderTemplateNodes(nodes, scope, options) {
|
|
472
|
+
const { escape } = options;
|
|
473
|
+
let result = "";
|
|
474
|
+
for (const node of nodes) {
|
|
475
|
+
if (node.type === "text") {
|
|
476
|
+
result += node.value;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const expression = parsed(node.slot, options.layer);
|
|
480
|
+
if (node.type === "value") {
|
|
481
|
+
const text = expression.kind === "Call" && SIGNING_DIRECTIVES.has(expression.callee) ? renderDirective(expression, node.slot, scope, options) : scope.present(expression, node.slot.source, node.slot.position);
|
|
482
|
+
result += text instanceof Markup ? text.value : node.escaped ? escape(text) : text;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const value = scope.evaluate(expression, node.slot.source, node.slot.position);
|
|
486
|
+
if (node.type === "if" || node.type === "unless") {
|
|
487
|
+
const answer = condition(value, node.slot, options.layer);
|
|
488
|
+
const include = node.type === "if" ? answer : !answer;
|
|
489
|
+
result += renderTemplateNodes(include ? node.children : node.inverse, scope, options);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const rows = loopRows(value, node.slot, options.layer);
|
|
493
|
+
if (rows.length === 0) {
|
|
494
|
+
result += renderTemplateNodes(node.inverse, scope, options);
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
for (const frame of loopFrames(scope, expression, rows, { kind: "each" })) {
|
|
498
|
+
result += renderTemplateNodes(node.children, scope.push(frame), options);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return result;
|
|
502
|
+
}
|
|
503
|
+
function escapeHtml(value) {
|
|
504
|
+
return value.replace(/[&<>"'`=]/g, (char) => ({
|
|
505
|
+
"&": "&",
|
|
506
|
+
"<": "<",
|
|
507
|
+
">": ">",
|
|
508
|
+
'"': """,
|
|
509
|
+
"'": "'",
|
|
510
|
+
"`": "`",
|
|
511
|
+
"=": "="
|
|
512
|
+
})[char]);
|
|
513
|
+
}
|
|
514
|
+
function renderTemplate(template, options) {
|
|
515
|
+
const scope = new TemplateScope(options.context, {
|
|
516
|
+
formatter: options.formatter,
|
|
517
|
+
resolveData: options.resolveData,
|
|
518
|
+
layer: options.layer
|
|
519
|
+
});
|
|
520
|
+
try {
|
|
521
|
+
return renderTemplateNodes(parseTemplate(template), scope, options);
|
|
522
|
+
} catch (error) {
|
|
523
|
+
throw error instanceof TemplateError ? error.inLayer(options.layer) : error;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// src/template/context.ts
|
|
528
|
+
import {
|
|
529
|
+
buildRegistry as buildRegistry2,
|
|
530
|
+
createContext
|
|
531
|
+
} from "@asterdoc/expr";
|
|
532
|
+
function requestExpressions(configured, supplied) {
|
|
533
|
+
return {
|
|
534
|
+
...configured,
|
|
535
|
+
...supplied && { context: supplied.context }
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function record(value) {
|
|
539
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
540
|
+
}
|
|
541
|
+
function templateRoots(raw, form) {
|
|
542
|
+
const nestedFields = record(raw.fields);
|
|
543
|
+
const fields = nestedFields ? { ...nestedFields } : {};
|
|
544
|
+
const items = form?.fields ? void 0 : record(raw.items);
|
|
545
|
+
const fieldIds = nestedFields ? [] : form?.fields ? Object.keys(form.fields) : Object.keys(raw).filter((key) => !RENDER_DATA_ROOTS.has(key) && !(key === "items" && items));
|
|
546
|
+
for (const id of fieldIds) if (Object.prototype.hasOwnProperty.call(raw, id)) fields[id] = raw[id];
|
|
547
|
+
const roots = { fields };
|
|
548
|
+
const parties = record(raw.parties);
|
|
549
|
+
if (parties) roots.parties = parties;
|
|
550
|
+
if (items) roots.items = items;
|
|
551
|
+
const defs = record(raw.defs);
|
|
552
|
+
if (defs) for (const [key, value] of Object.entries(defs)) roots[key] = value;
|
|
553
|
+
return roots;
|
|
554
|
+
}
|
|
555
|
+
function withConfiguredFunctions(context, expressions) {
|
|
556
|
+
if (!expressions.functions && !expressions.signatures) return context;
|
|
557
|
+
const configured = buildRegistry2(expressions.signatures ?? []);
|
|
558
|
+
const signatures = new Map(configured.signatures);
|
|
559
|
+
for (const [name, signature] of context.registry?.signatures ?? []) signatures.set(name, signature);
|
|
560
|
+
const registry = {
|
|
561
|
+
signatures,
|
|
562
|
+
has: (name) => signatures.has(name),
|
|
563
|
+
get: (name) => signatures.get(name),
|
|
564
|
+
names: () => [...signatures.keys()]
|
|
565
|
+
};
|
|
566
|
+
return { ...context, registry, hostFunctions: { ...expressions.functions, ...context.hostFunctions } };
|
|
567
|
+
}
|
|
568
|
+
function templateData(raw, prepared, form, expressions) {
|
|
569
|
+
const context = expressions?.context ? withConfiguredFunctions(expressions.context, expressions) : createContext(templateRoots(raw, form), {
|
|
570
|
+
hostFunctions: expressions?.functions,
|
|
571
|
+
registry: expressions?.signatures ? buildRegistry2(expressions.signatures) : void 0
|
|
572
|
+
});
|
|
573
|
+
const defs = record(prepared.defs);
|
|
574
|
+
return {
|
|
575
|
+
context,
|
|
576
|
+
resolveData(segments) {
|
|
577
|
+
const [head, ...rest] = segments;
|
|
578
|
+
if (head === void 0) return void 0;
|
|
579
|
+
if (head === "fields") {
|
|
580
|
+
if (rest.length === 0) return void 0;
|
|
581
|
+
return dataPath(record(prepared.fields) ?? prepared, rest);
|
|
582
|
+
}
|
|
583
|
+
if (head === "parties" || head === "annexes" || head === "items") return dataPath(prepared[head], rest);
|
|
584
|
+
if (defs && Object.prototype.hasOwnProperty.call(defs, head)) return dataPath(defs[head], rest);
|
|
585
|
+
return void 0;
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/text/signatures.ts
|
|
591
|
+
var defaults = {
|
|
592
|
+
signature: "[SIGNATURE]",
|
|
593
|
+
initials: "[INITIALS]",
|
|
594
|
+
date: "[DATE]",
|
|
595
|
+
capacity: "[CAPACITY]",
|
|
596
|
+
printedName: "[PRINTED NAME]",
|
|
597
|
+
capturedSignature: "[Signed]",
|
|
598
|
+
capturedInitials: "[Initialed]"
|
|
599
|
+
};
|
|
600
|
+
function resolveValue(value, context, fallback) {
|
|
601
|
+
return value === void 0 ? fallback : typeof value === "function" ? value(context) : value;
|
|
602
|
+
}
|
|
603
|
+
function isRenderParty(value) {
|
|
604
|
+
return typeof value._role === "string" && typeof value.id === "string";
|
|
605
|
+
}
|
|
606
|
+
var ORGANIZATION_KEYS = ["legalName", "domicile", "entityType", "entityId", "taxId"];
|
|
607
|
+
function selfSignerId(party) {
|
|
608
|
+
return ORGANIZATION_KEYS.some((key) => key in party) ? "" : party.id;
|
|
609
|
+
}
|
|
610
|
+
function resolveContext(value) {
|
|
611
|
+
if (!value || typeof value !== "object") return void 0;
|
|
612
|
+
const context = value;
|
|
613
|
+
if (typeof context._role === "string" && typeof context._partyId === "string" && typeof context.signerId === "string") {
|
|
614
|
+
return {
|
|
615
|
+
role: context._role,
|
|
616
|
+
partyId: context._partyId,
|
|
617
|
+
signerId: context.signerId,
|
|
618
|
+
signer: context.signer,
|
|
619
|
+
capacity: context.capacity
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
if (isRenderParty(context) && !context.signerId) {
|
|
623
|
+
const signatory = Array.isArray(context.signatories) ? context.signatories[0] : void 0;
|
|
624
|
+
return {
|
|
625
|
+
role: context._role,
|
|
626
|
+
partyId: context.id,
|
|
627
|
+
signerId: typeof signatory?.signerId === "string" ? signatory.signerId : selfSignerId(context),
|
|
628
|
+
party: context,
|
|
629
|
+
signer: signatory?.signer,
|
|
630
|
+
capacity: signatory?.capacity
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
return void 0;
|
|
634
|
+
}
|
|
635
|
+
function captureFor(root, context, locationId, type) {
|
|
636
|
+
if (!Array.isArray(root._captures)) return void 0;
|
|
637
|
+
return root._captures.find(
|
|
638
|
+
(capture) => capture.role === context.role && capture.partyId === context.partyId && capture.signerId === context.signerId && capture.locationId === locationId && capture.type === type
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
function signerFor(root, context) {
|
|
642
|
+
if (context.signer) return context.signer;
|
|
643
|
+
return root._signers && typeof root._signers === "object" ? root._signers[context.signerId] : void 0;
|
|
644
|
+
}
|
|
645
|
+
function placeholderContext(root, context, locationId) {
|
|
646
|
+
return {
|
|
647
|
+
role: context.role,
|
|
648
|
+
partyId: context.partyId,
|
|
649
|
+
signerId: context.signerId,
|
|
650
|
+
locationId,
|
|
651
|
+
party: context.party,
|
|
652
|
+
signer: signerFor(root, context),
|
|
653
|
+
capacity: context.capacity
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
function markAttributes(context, locationId) {
|
|
657
|
+
return `data-role="${escapeHtml(context.role)}" data-party-id="${escapeHtml(context.partyId)}" data-signer-id="${escapeHtml(context.signerId)}" data-location-id="${escapeHtml(locationId)}"`;
|
|
658
|
+
}
|
|
659
|
+
function markdownAlt(value) {
|
|
660
|
+
return value.replace(/[\\[\]]/g, (char) => `\\${char}`);
|
|
661
|
+
}
|
|
662
|
+
function markdownDestination(value) {
|
|
663
|
+
return value.replace(/[\s()<>\\]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`);
|
|
664
|
+
}
|
|
665
|
+
function contextError(name) {
|
|
666
|
+
return `[${name} error: no party. Use it inside a party or signatories loop, or pass the party first.]`;
|
|
667
|
+
}
|
|
668
|
+
function renderMark(type, capture, context, options, signatureDefaults) {
|
|
669
|
+
const captured = capture !== void 0;
|
|
670
|
+
const capturedOption = type === "signature" ? options.captured?.signature : options.captured?.initials;
|
|
671
|
+
const placeholderOption = type === "signature" ? options.placeholder?.signature : options.placeholder?.initials;
|
|
672
|
+
const capturedDefault = type === "signature" ? signatureDefaults.capturedSignature : signatureDefaults.capturedInitials;
|
|
673
|
+
const placeholderDefault = type === "signature" ? signatureDefaults.signature : signatureDefaults.initials;
|
|
674
|
+
const capturedContext = capture ? { ...context, capture } : void 0;
|
|
675
|
+
if (options.format === "html") {
|
|
676
|
+
const adopted = type === "signature" ? context.signer?.adopted?.signature : context.signer?.adopted?.initials;
|
|
677
|
+
const image = capture?.image ?? adopted?.image;
|
|
678
|
+
const attributes = markAttributes(context, context.locationId);
|
|
679
|
+
if (captured && image) {
|
|
680
|
+
const alt = options.altText ?? (type === "signature" ? "Signature" : "Initials");
|
|
681
|
+
const cssClass = options.cssClass ?? `${type}-image`;
|
|
682
|
+
return new Markup(`<img src="${escapeHtml(image)}" alt="${escapeHtml(alt)}" class="${escapeHtml(cssClass)}" ${attributes} />`);
|
|
683
|
+
}
|
|
684
|
+
const text = capturedContext ? resolveValue(capturedOption, capturedContext, capturedDefault) : resolveValue(placeholderOption, context, placeholderDefault);
|
|
685
|
+
return new Markup(`<span class="${type}-${captured ? "captured" : "placeholder"}" ${attributes}>${text}</span>`);
|
|
686
|
+
}
|
|
687
|
+
if (options.format === "markdown") {
|
|
688
|
+
const adopted = type === "signature" ? context.signer?.adopted?.signature : context.signer?.adopted?.initials;
|
|
689
|
+
const image = capture?.image ?? adopted?.image;
|
|
690
|
+
if (captured && image) {
|
|
691
|
+
const alt = options.altText ?? (type === "signature" ? "Signature" : "Initials");
|
|
692
|
+
return `})`;
|
|
693
|
+
}
|
|
694
|
+
const text = capturedContext ? resolveValue(capturedOption, capturedContext, capturedDefault) : resolveValue(placeholderOption, context, placeholderDefault);
|
|
695
|
+
return `_${text}_`;
|
|
696
|
+
}
|
|
697
|
+
return capturedContext ? resolveValue(capturedOption, capturedContext, capturedDefault) : resolveValue(placeholderOption, context, placeholderDefault);
|
|
698
|
+
}
|
|
699
|
+
function createMarkDirective(type, options, signatureDefaults) {
|
|
700
|
+
return (value, root, args) => {
|
|
701
|
+
const context = resolveContext(value);
|
|
702
|
+
if (!context) return contextError(type === "signature" ? "Signature" : "Initials");
|
|
703
|
+
const locationId = args[0];
|
|
704
|
+
const capture = context.signerId ? captureFor(root, context, locationId, type) : void 0;
|
|
705
|
+
return renderMark(type, capture, placeholderContext(root, context, locationId), options, signatureDefaults);
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
function createDateDirective(options, signatureDefaults) {
|
|
709
|
+
return (value, root, args) => {
|
|
710
|
+
const context = resolveContext(value);
|
|
711
|
+
if (!context) return contextError("SignatureDate");
|
|
712
|
+
const locationId = args[0];
|
|
713
|
+
const placeholder = placeholderContext(root, context, locationId);
|
|
714
|
+
const capture = context.signerId ? captureFor(root, context, locationId, "signature") : void 0;
|
|
715
|
+
if (!capture) {
|
|
716
|
+
const text2 = resolveValue(options.placeholder?.signatureDate, placeholder, signatureDefaults.date);
|
|
717
|
+
if (options.format !== "html") return text2;
|
|
718
|
+
return new Markup(`<span class="signature-date-placeholder" ${markAttributes(context, locationId)}>${text2}</span>`);
|
|
719
|
+
}
|
|
720
|
+
const captured = { ...placeholder, capture };
|
|
721
|
+
const date = capture.timestamp ? capture.timestamp.slice(0, 10) : signatureDefaults.date;
|
|
722
|
+
const text = resolveValue(options.captured?.signatureDate, captured, date);
|
|
723
|
+
if (options.format !== "html" || options.captured?.signatureDate !== void 0) return text;
|
|
724
|
+
return new Markup(`<span class="signature-date" ${markAttributes(context, locationId)}>${escapeHtml(text)}</span>`);
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function createCapacityDirective(options, signatureDefaults) {
|
|
728
|
+
return (value, root, args) => {
|
|
729
|
+
const context = resolveContext(value);
|
|
730
|
+
if (!context) return contextError("Capacity");
|
|
731
|
+
const locationId = args[0];
|
|
732
|
+
const placeholder = placeholderContext(root, context, locationId);
|
|
733
|
+
const capture = context.signerId ? captureFor(root, context, locationId, "capacity") : void 0;
|
|
734
|
+
if (capture) {
|
|
735
|
+
const fallback = capture.text ?? context.capacity ?? signatureDefaults.capacity;
|
|
736
|
+
return resolveValue(options.captured?.capacity, { ...placeholder, capture }, fallback);
|
|
737
|
+
}
|
|
738
|
+
return context.capacity ?? resolveValue(options.placeholder?.capacity, placeholder, signatureDefaults.capacity);
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
function createPrintedNameDirective(options, signatureDefaults) {
|
|
742
|
+
return (value, root, args) => {
|
|
743
|
+
const context = resolveContext(value);
|
|
744
|
+
if (!context) return contextError("PrintedName");
|
|
745
|
+
const locationId = args[0];
|
|
746
|
+
const placeholder = placeholderContext(root, context, locationId);
|
|
747
|
+
const capture = context.signerId ? captureFor(root, context, locationId, "printed_name") : void 0;
|
|
748
|
+
if (capture) {
|
|
749
|
+
const fallback = capture.text ?? placeholder.signer?.person.name ?? signatureDefaults.printedName;
|
|
750
|
+
return resolveValue(options.captured?.printedName, { ...placeholder, capture }, fallback);
|
|
751
|
+
}
|
|
752
|
+
return placeholder.signer?.person.name ?? resolveValue(options.placeholder?.printedName, placeholder, signatureDefaults.printedName);
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
function createSignatureDirectives(options = {}, signatureDefaults = defaults) {
|
|
756
|
+
return {
|
|
757
|
+
signature: createMarkDirective("signature", options, signatureDefaults),
|
|
758
|
+
initials: createMarkDirective("initials", options, signatureDefaults),
|
|
759
|
+
signatureDate: createDateDirective(options, signatureDefaults),
|
|
760
|
+
capacity: createCapacityDirective(options, signatureDefaults),
|
|
761
|
+
printedName: createPrintedNameDirective(options, signatureDefaults)
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
function formatFor(mimeType) {
|
|
765
|
+
const type = mimeType?.toLowerCase();
|
|
766
|
+
return type === "text/html" ? "html" : type === "text/markdown" ? "markdown" : "text";
|
|
767
|
+
}
|
|
768
|
+
function createTextSignatureDirectives(options = {}, mimeType) {
|
|
769
|
+
return createSignatureDirectives({ ...options, format: options.format ?? formatFor(mimeType) });
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// src/docx/signatures.ts
|
|
773
|
+
var docxDefaults = {
|
|
774
|
+
signature: "_____________________________",
|
|
775
|
+
initials: "______",
|
|
776
|
+
date: "__________",
|
|
777
|
+
capacity: "________________",
|
|
778
|
+
printedName: "_______________________",
|
|
779
|
+
capturedSignature: "[Signed]",
|
|
780
|
+
capturedInitials: "[Initialed]"
|
|
781
|
+
};
|
|
782
|
+
function createDocxSignatureDirectives(options = {}) {
|
|
783
|
+
return createSignatureDirectives(options, docxDefaults);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/docx/render.ts
|
|
787
|
+
var textDecoder = new TextDecoder();
|
|
788
|
+
var textEncoder = new TextEncoder();
|
|
789
|
+
function decodeXml(value) {
|
|
790
|
+
return value.replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCodePoint(Number.parseInt(code, 16))).replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code))).replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
791
|
+
}
|
|
792
|
+
function encodeXml(value) {
|
|
793
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
794
|
+
}
|
|
795
|
+
function regexEscape(value) {
|
|
796
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
797
|
+
}
|
|
798
|
+
function normalizeDelimiters(value, delimiters) {
|
|
799
|
+
if (delimiters[0] === "{{" && delimiters[1] === "}}") return value;
|
|
800
|
+
return value.replace(new RegExp(regexEscape(delimiters[0]), "g"), "{{").replace(new RegExp(regexEscape(delimiters[1]), "g"), "}}");
|
|
801
|
+
}
|
|
802
|
+
function paragraphText(paragraph) {
|
|
803
|
+
return [...paragraph.matchAll(/<w:t\b[^>]*>([\s\S]*?)<\/w:t>/g)].map((match) => decodeXml(match[1] ?? "")).join("");
|
|
804
|
+
}
|
|
805
|
+
function visibleParagraph(paragraph) {
|
|
806
|
+
return paragraphText(paragraph).trim();
|
|
807
|
+
}
|
|
808
|
+
function commandIn(paragraph, delimiters) {
|
|
809
|
+
const value = visibleParagraph(paragraph);
|
|
810
|
+
if (!value.startsWith(delimiters[0]) || !value.endsWith(delimiters[1])) return void 0;
|
|
811
|
+
return value.slice(delimiters[0].length, -delimiters[1].length).trim();
|
|
812
|
+
}
|
|
813
|
+
function controlsIn(xml, delimiters) {
|
|
814
|
+
const rows = [...xml.matchAll(/<w:tr\b[^>]*>[\s\S]*?<\/w:tr>/g)].map((match) => ({
|
|
815
|
+
start: match.index ?? 0,
|
|
816
|
+
end: (match.index ?? 0) + match[0].length,
|
|
817
|
+
value: visibleParagraph(match[0])
|
|
818
|
+
}));
|
|
819
|
+
return [...xml.matchAll(/<w:p\b[^>]*>[\s\S]*?<\/w:p>/g)].map((match) => {
|
|
820
|
+
const start = match.index ?? 0;
|
|
821
|
+
const end = start + match[0].length;
|
|
822
|
+
const command = commandIn(match[0], delimiters);
|
|
823
|
+
const marker = command === void 0 ? void 0 : `${delimiters[0]}${command}${delimiters[1]}`;
|
|
824
|
+
const row = marker === void 0 ? void 0 : rows.find((candidate) => candidate.start <= start && candidate.end >= end && candidate.value === marker);
|
|
825
|
+
return { command, start: row?.start ?? start, end: row?.end ?? end };
|
|
826
|
+
}).filter((item) => item.command !== void 0);
|
|
827
|
+
}
|
|
828
|
+
function normalizeInlineControls(xml, delimiters) {
|
|
829
|
+
const paragraphPattern = /<w:p\b[^>]*>[\s\S]*?<\/w:p>/g;
|
|
830
|
+
const [open, close] = delimiters.map(regexEscape);
|
|
831
|
+
const command = (body) => `${open}\\s*(?:${body})(?:(?!${close})[\\s\\S])*?${close}`;
|
|
832
|
+
const inlinePattern = new RegExp(`^\\s*${command("(?:FOR|IF)\\s")}[\\s\\S]*${command("END-(?:FOR|IF)")}\\s*$`);
|
|
833
|
+
const anyCommand = new RegExp(`(${command("(?:FOR|IF)\\s|ELSE\\s*(?=" + close + ")|END-(?:FOR|IF)")})`);
|
|
834
|
+
return xml.replace(paragraphPattern, (paragraph) => {
|
|
835
|
+
const text = paragraphText(paragraph);
|
|
836
|
+
if (!inlinePattern.test(text)) return paragraph;
|
|
837
|
+
const opening = paragraph.match(/^<w:p\b[^>]*>/)?.[0] ?? "<w:p>";
|
|
838
|
+
const properties = paragraph.match(/<w:pPr\b[\s\S]*?<\/w:pPr>/)?.[0] ?? "";
|
|
839
|
+
const makeParagraph = (text2) => {
|
|
840
|
+
const content = text2.length > 0 ? `<w:r><w:t xml:space="preserve">${encodeXml(text2)}</w:t></w:r>` : "";
|
|
841
|
+
return `${opening}${properties}${content}</w:p>`;
|
|
842
|
+
};
|
|
843
|
+
return text.split(anyCommand).filter((piece) => piece.length > 0).map(makeParagraph).join("");
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
var OPENING = /^(?:FOR\s+\S+\s+IN\s+.+|IF\s+.+)$/;
|
|
847
|
+
var CLOSING = /^END-(?:FOR|IF)(?:\s+\S+)?$/;
|
|
848
|
+
function docxControlProblems(commands) {
|
|
849
|
+
const problems = [];
|
|
850
|
+
const open = [];
|
|
851
|
+
commands.forEach((command, index) => {
|
|
852
|
+
if (command === void 0) return;
|
|
853
|
+
if (OPENING.test(command)) {
|
|
854
|
+
const kind = command.startsWith("FOR ") ? "FOR" : "IF";
|
|
855
|
+
open.push({ index, command, kind, alias: kind === "FOR" ? command.split(/\s+/)[1] : void 0, hasElse: false });
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (CLOSING.test(command)) {
|
|
859
|
+
const [closer, alias] = command.split(/\s+/);
|
|
860
|
+
const block = open.pop();
|
|
861
|
+
if (!block) problems.push({ index, message: `Unexpected ${command}: no block is open.` });
|
|
862
|
+
else if (closer !== `END-${block.kind}`) problems.push({ index, message: `${command} cannot close ${block.command}; write END-${block.kind}.` });
|
|
863
|
+
else if (alias !== void 0 && alias !== block.alias) problems.push({ index, message: `${command} cannot close ${block.command}; write END-FOR ${block.alias}.` });
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (command === "ELSE") {
|
|
867
|
+
const block = open.at(-1);
|
|
868
|
+
if (block?.kind !== "IF") problems.push({ index, message: "ELSE belongs inside an IF block." });
|
|
869
|
+
else if (block.hasElse) problems.push({ index, message: `${block.command} has a second ELSE; an IF takes one.` });
|
|
870
|
+
else block.hasElse = true;
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
for (const block of open) problems.push({ index: block.index, message: `Unclosed DOCX control command: ${block.command}` });
|
|
874
|
+
return problems.sort((a, b) => a.index - b.index);
|
|
875
|
+
}
|
|
876
|
+
var PARAGRAPH_MARK = /<!--pdc:(\d+)-->/;
|
|
877
|
+
function markParagraphs(xml) {
|
|
878
|
+
let count = 0;
|
|
879
|
+
return xml.replace(/<w:p\b[^>]*>/g, (open) => `${open}<!--pdc:${++count}-->`);
|
|
880
|
+
}
|
|
881
|
+
function unmarkParagraphs(xml) {
|
|
882
|
+
return xml.replace(/<!--pdc:\d+-->/g, "");
|
|
883
|
+
}
|
|
884
|
+
function locate(run, xml) {
|
|
885
|
+
const paragraph = xml.match(PARAGRAPH_MARK)?.[1];
|
|
886
|
+
return paragraph ? `${run.part} paragraph ${paragraph}` : run.part;
|
|
887
|
+
}
|
|
888
|
+
function located(run, xml, render) {
|
|
889
|
+
try {
|
|
890
|
+
return render();
|
|
891
|
+
} catch (error) {
|
|
892
|
+
if (!(error instanceof TemplateError) || error.location) throw error;
|
|
893
|
+
throw new TemplateError({ ...error.diagnostic, location: locate(run, xml) }, run.options.layer);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
function expandControls(xml, run) {
|
|
897
|
+
const controls = controlsIn(xml, run.delimiters);
|
|
898
|
+
const openingIndex = controls.findIndex(({ command }) => OPENING.test(command));
|
|
899
|
+
if (openingIndex === -1) return renderLeafXml(xml, run);
|
|
900
|
+
const opening = controls[openingIndex];
|
|
901
|
+
const isFor = opening.command.startsWith("FOR ");
|
|
902
|
+
let depth = 1;
|
|
903
|
+
let closing;
|
|
904
|
+
let alternative;
|
|
905
|
+
for (const control of controls.slice(openingIndex + 1)) {
|
|
906
|
+
if (OPENING.test(control.command)) depth++;
|
|
907
|
+
else if (CLOSING.test(control.command)) {
|
|
908
|
+
depth--;
|
|
909
|
+
if (depth === 0) {
|
|
910
|
+
closing = control;
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
} else if (control.command === "ELSE" && depth === 1) alternative = control;
|
|
914
|
+
}
|
|
915
|
+
const openingXml = xml.slice(opening.start, opening.end);
|
|
916
|
+
const before = xml.slice(0, opening.start);
|
|
917
|
+
const close = closing;
|
|
918
|
+
const truthyBody = xml.slice(opening.end, alternative?.start ?? close.start);
|
|
919
|
+
const falseBody = alternative ? xml.slice(alternative.end, close.start) : "";
|
|
920
|
+
const expanded = located(run, openingXml, () => {
|
|
921
|
+
if (isFor) {
|
|
922
|
+
const match = opening.command.match(/^FOR\s+(\S+)\s+IN\s+(.+)$/);
|
|
923
|
+
const alias = match[1];
|
|
924
|
+
const slot2 = expressionSlot(match[2], { line: 1, column: 1 });
|
|
925
|
+
if (!slot2.ast) throw new TemplateError(slot2.problem, run.options.layer);
|
|
926
|
+
const rows = loopRows(run.scope.evaluate(slot2.ast, slot2.source, slot2.position), slot2, run.options.layer);
|
|
927
|
+
return loopFrames(run.scope, slot2.ast, rows, { kind: "named", name: alias }).map((frame) => expandControls(truthyBody, { ...run, scope: run.scope.push(frame) })).join("");
|
|
928
|
+
}
|
|
929
|
+
const slot = expressionSlot(opening.command.slice(3), { line: 1, column: 1 });
|
|
930
|
+
if (!slot.ast) throw new TemplateError(slot.problem, run.options.layer);
|
|
931
|
+
const value = run.scope.evaluate(slot.ast, slot.source, slot.position);
|
|
932
|
+
if (value.kind !== "boolean" && value.kind !== "null") {
|
|
933
|
+
throw new TemplateError({ code: "non-boolean-gate", message: `A condition must be boolean, got ${value.kind}.`, expression: slot.source }, run.options.layer);
|
|
934
|
+
}
|
|
935
|
+
return expandControls(value.kind === "boolean" && value.value ? truthyBody : falseBody, run);
|
|
936
|
+
});
|
|
937
|
+
return `${renderLeafXml(before, run)}${expanded}${expandControls(xml.slice(close.end), run)}`;
|
|
938
|
+
}
|
|
939
|
+
function validateControls(xml, run) {
|
|
940
|
+
const controls = controlsIn(xml, run.delimiters);
|
|
941
|
+
const [problem] = docxControlProblems(controls.map(({ command }) => command));
|
|
942
|
+
if (!problem) return;
|
|
943
|
+
const control = controls[problem.index];
|
|
944
|
+
throw new TemplateError({ code: "markers", message: problem.message, location: locate(run, xml.slice(control.start, control.end)) }, run.options.layer);
|
|
945
|
+
}
|
|
946
|
+
function renderRunText(text, run) {
|
|
947
|
+
const nodes = parseTemplate(normalizeDelimiters(text, run.delimiters));
|
|
948
|
+
const rendered = encodeXml(renderTemplateNodes(nodes, run.scope, run.options));
|
|
949
|
+
return run.processLineBreaks ? rendered.replace(/\r?\n/g, '</w:t><w:br/><w:t xml:space="preserve">') : rendered;
|
|
950
|
+
}
|
|
951
|
+
function renderLeafXml(xml, run) {
|
|
952
|
+
const { delimiters } = run;
|
|
953
|
+
const textNode = /(<w:t\b[^>]*>)([\s\S]*?)(<\/w:t>)/g;
|
|
954
|
+
return xml.replace(/<w:p\b[^>]*>[\s\S]*?<\/w:p>/g, (paragraph) => located(run, paragraph, () => {
|
|
955
|
+
const nodes = [...paragraph.matchAll(textNode)];
|
|
956
|
+
const texts = nodes.map((match) => decodeXml(match[2] ?? ""));
|
|
957
|
+
const visible = texts.join("");
|
|
958
|
+
let offset = 0;
|
|
959
|
+
const boundaries = texts.slice(0, -1).map((text) => offset += text.length);
|
|
960
|
+
const commandPattern = new RegExp(`${regexEscape(delimiters[0])}[\\s\\S]*?${regexEscape(delimiters[1])}`, "g");
|
|
961
|
+
const hasSplitCommand = [...visible.matchAll(commandPattern)].some((match) => boundaries.some((boundary) => boundary > match.index && boundary < match.index + match[0].length));
|
|
962
|
+
if (!hasSplitCommand) {
|
|
963
|
+
return paragraph.replace(textNode, (_, open, content, close) => `${open}${renderRunText(decodeXml(content), run)}${close}`);
|
|
964
|
+
}
|
|
965
|
+
const replacement = renderRunText(visible, run);
|
|
966
|
+
let used = false;
|
|
967
|
+
return paragraph.replace(textNode, (_, open, _content, close) => {
|
|
968
|
+
if (used) return `${open}${close}`;
|
|
969
|
+
used = true;
|
|
970
|
+
return `${open}${replacement}${close}`;
|
|
971
|
+
});
|
|
972
|
+
}));
|
|
973
|
+
}
|
|
974
|
+
async function renderDocx({
|
|
975
|
+
template,
|
|
976
|
+
data,
|
|
977
|
+
form,
|
|
978
|
+
formatter = defaultFormatter,
|
|
979
|
+
signatureOptions,
|
|
980
|
+
options = {},
|
|
981
|
+
expressions,
|
|
982
|
+
layer
|
|
983
|
+
}) {
|
|
984
|
+
const prepared = form ? formatFieldData(data, form, formatter) : data;
|
|
985
|
+
const { context, resolveData } = templateData(data, prepared, form, expressions);
|
|
986
|
+
const directives = createDocxSignatureDirectives(signatureOptions);
|
|
987
|
+
const templateOptions = {
|
|
988
|
+
context,
|
|
989
|
+
resolveData,
|
|
990
|
+
root: prepared,
|
|
991
|
+
formatter,
|
|
992
|
+
directives,
|
|
993
|
+
escape: (text) => text,
|
|
994
|
+
layer
|
|
995
|
+
};
|
|
996
|
+
const scope = new TemplateScope(context, { formatter, resolveData, layer });
|
|
997
|
+
const files = unzipSync(template);
|
|
998
|
+
const delimiters = options.cmdDelimiter ?? ["{{", "}}"];
|
|
999
|
+
const processLineBreaks = options.processLineBreaks ?? true;
|
|
1000
|
+
for (const [name, bytes] of Object.entries(files)) {
|
|
1001
|
+
if (!DOCX_TEMPLATE_PARTS.test(name)) continue;
|
|
1002
|
+
const xml = markParagraphs(normalizeInlineControls(textDecoder.decode(bytes), delimiters));
|
|
1003
|
+
const run = { scope, options: templateOptions, delimiters, processLineBreaks, part: name };
|
|
1004
|
+
validateControls(xml, run);
|
|
1005
|
+
files[name] = textEncoder.encode(unmarkParagraphs(expandControls(xml, run)));
|
|
1006
|
+
}
|
|
1007
|
+
return zipSync(files, { level: 6 });
|
|
1008
|
+
}
|
|
1009
|
+
var DOCX_TEMPLATE_PARTS = /^word\/(?:document|header\d*|footer\d*|footnotes|endnotes)\.xml$/;
|
|
1010
|
+
|
|
1011
|
+
// src/template/check.ts
|
|
1012
|
+
import { unzipSync as unzipSync2 } from "fflate";
|
|
1013
|
+
import { checkAst, formatType, T as T2 } from "@asterdoc/expr";
|
|
1014
|
+
var CheckScope = class _CheckScope {
|
|
1015
|
+
constructor(env, frames = []) {
|
|
1016
|
+
this.env = env;
|
|
1017
|
+
this.frames = frames;
|
|
1018
|
+
}
|
|
1019
|
+
env;
|
|
1020
|
+
frames;
|
|
1021
|
+
push(frame) {
|
|
1022
|
+
return new _CheckScope(this.env, [...this.frames, frame]);
|
|
1023
|
+
}
|
|
1024
|
+
frameFor(name) {
|
|
1025
|
+
return frameFor(this.frames, name);
|
|
1026
|
+
}
|
|
1027
|
+
typeEnv() {
|
|
1028
|
+
if (this.frames.length === 0) return this.env;
|
|
1029
|
+
return {
|
|
1030
|
+
registry: withLoopFunctions(this.env.registry),
|
|
1031
|
+
resolve: (path) => {
|
|
1032
|
+
const [head, ...rest] = path.split(".");
|
|
1033
|
+
const frame = head ? this.frameFor(head) : void 0;
|
|
1034
|
+
if (!frame) return this.env.resolve(path);
|
|
1035
|
+
if (rest.length === 0) return frame.element;
|
|
1036
|
+
if (frame.sourcePath) return this.env.resolve([frame.sourcePath, ...rest].join("."));
|
|
1037
|
+
return T2.unknown;
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
/** The environment path a list expression names, so its rows' members can be typed. */
|
|
1042
|
+
sourcePath(node) {
|
|
1043
|
+
const segments = referenceSegments(node);
|
|
1044
|
+
if (!segments || segments.some((segment) => /^\d+$/.test(segment))) return void 0;
|
|
1045
|
+
const [head, ...rest] = segments;
|
|
1046
|
+
const frame = head ? this.frameFor(head) : void 0;
|
|
1047
|
+
if (frame) return frame.sourcePath ? [frame.sourcePath, ...rest].join(".") : void 0;
|
|
1048
|
+
return segments.join(".");
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
function at(slot, span) {
|
|
1052
|
+
return span.start.line === 1 ? { line: slot.position.line, column: slot.position.column + span.start.column - 1 } : { line: slot.position.line + span.start.line - 1, column: span.start.column };
|
|
1053
|
+
}
|
|
1054
|
+
function diagnosticsFor(slot, node, scope) {
|
|
1055
|
+
const result = checkAst(node, scope.typeEnv());
|
|
1056
|
+
const diagnostics = result.diagnostics.map((diagnostic) => {
|
|
1057
|
+
let message = diagnostic.message;
|
|
1058
|
+
const unknown = diagnostic.name;
|
|
1059
|
+
if (diagnostic.code === "unknown-function" && unknown && LOOP_FUNCTION_NAMES.has(unknown)) {
|
|
1060
|
+
message = `${unknown}() is valid only inside a loop.`;
|
|
1061
|
+
} else if (diagnostic.code === "unknown-identifier" && (unknown === "item" || unknown === "parent")) {
|
|
1062
|
+
message = unknown === "item" ? "item is valid only inside a loop." : "parent is valid only inside a nested loop.";
|
|
1063
|
+
} else if (diagnostic.code === "unknown-identifier" && unknown?.split(".")[0] === "this") {
|
|
1064
|
+
message = removedSyntaxHint("this");
|
|
1065
|
+
}
|
|
1066
|
+
return { code: diagnostic.code, message, expression: slot.source, position: at(slot, diagnostic.span) };
|
|
1067
|
+
});
|
|
1068
|
+
diagnostics.push(...loopFunctionMisuse(slot, node, scope));
|
|
1069
|
+
return { type: result.type, diagnostics };
|
|
1070
|
+
}
|
|
1071
|
+
function loopFunctionMisuse(slot, node, scope) {
|
|
1072
|
+
const found = [];
|
|
1073
|
+
const visit = (current) => {
|
|
1074
|
+
if (current.kind === "Call") {
|
|
1075
|
+
if (LOOP_FUNCTION_NAMES.has(current.callee) && scope.frames.length > 0) {
|
|
1076
|
+
const arg = current.args[0];
|
|
1077
|
+
if (current.args.length !== 1 || arg?.kind !== "Identifier" || !scope.frameFor(arg.name)) {
|
|
1078
|
+
found.push({ code: "type-mismatch", message: `${current.callee}() takes a loop row, such as ${current.callee}(item).`, expression: slot.source, position: at(slot, current.span) });
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
current.args.forEach(visit);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
for (const value of Object.values(current)) {
|
|
1085
|
+
if (value && typeof value === "object" && "kind" in value && "span" in value) visit(value);
|
|
1086
|
+
if (Array.isArray(value)) value.forEach((element) => element && typeof element === "object" && "kind" in element && visit(element));
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
visit(node);
|
|
1090
|
+
return found;
|
|
1091
|
+
}
|
|
1092
|
+
function checkDirective(slot, node, scope) {
|
|
1093
|
+
const found = [];
|
|
1094
|
+
if (node.args.length < 1 || node.args.length > 2) {
|
|
1095
|
+
found.push({ code: "arity", message: `${node.callee} takes a location, or a party and a location, such as ${node.callee}(parties.tenant, "tenant-sign").`, expression: slot.source, position: slot.position });
|
|
1096
|
+
return found;
|
|
1097
|
+
}
|
|
1098
|
+
if (node.args.length === 1 && scope.frames.length === 0) {
|
|
1099
|
+
found.push({ code: "arity", message: `${node.callee}() needs a party: use it inside a party loop, or pass the party first, such as ${node.callee}(parties.tenant, "location").`, expression: slot.source, position: slot.position });
|
|
1100
|
+
}
|
|
1101
|
+
for (const arg of node.args) found.push(...diagnosticsFor(slot, arg, scope).diagnostics);
|
|
1102
|
+
const location = diagnosticsFor(slot, node.args.at(-1), scope).type;
|
|
1103
|
+
if (location.kind !== "string" && location.kind !== "unknown") {
|
|
1104
|
+
found.push({ code: "type-mismatch", message: `${node.callee}'s location must be a string, got ${formatType(location)}.`, expression: slot.source, position: slot.position });
|
|
1105
|
+
}
|
|
1106
|
+
return found;
|
|
1107
|
+
}
|
|
1108
|
+
function checkSlot(slot, scope, role) {
|
|
1109
|
+
if (!slot.ast) return { diagnostics: slot.problem ? [slot.problem] : [] };
|
|
1110
|
+
if (role === "value" && slot.ast.kind === "Call" && SIGNING_DIRECTIVES.has(slot.ast.callee)) {
|
|
1111
|
+
return { diagnostics: checkDirective(slot, slot.ast, scope) };
|
|
1112
|
+
}
|
|
1113
|
+
const { type, diagnostics } = diagnosticsFor(slot, slot.ast, scope);
|
|
1114
|
+
if (diagnostics.length > 0) return { diagnostics };
|
|
1115
|
+
if (role === "condition" && type.kind !== "boolean") {
|
|
1116
|
+
return {
|
|
1117
|
+
diagnostics: [{
|
|
1118
|
+
code: "non-boolean-gate",
|
|
1119
|
+
message: type.kind === "unknown" ? "A condition must be boolean; its type cannot be determined. Compare it, such as value != null." : `A condition must be boolean, got ${formatType(type)}. Compare it, such as value != null.`,
|
|
1120
|
+
expression: slot.source,
|
|
1121
|
+
position: slot.position
|
|
1122
|
+
}]
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
if (role === "loop") {
|
|
1126
|
+
if (type.kind !== "array") {
|
|
1127
|
+
return { diagnostics: [{ code: "type-mismatch", message: `A loop source must be a list, got ${formatType(type)}.`, expression: slot.source, position: slot.position }] };
|
|
1128
|
+
}
|
|
1129
|
+
return { diagnostics: [], frame: { kind: "each", element: type.element, sourcePath: scope.sourcePath(slot.ast) } };
|
|
1130
|
+
}
|
|
1131
|
+
return { diagnostics: [] };
|
|
1132
|
+
}
|
|
1133
|
+
function checkNodes(nodes, scope, found) {
|
|
1134
|
+
for (const node of nodes) {
|
|
1135
|
+
if (node.type === "text") continue;
|
|
1136
|
+
if (node.type === "value") {
|
|
1137
|
+
found.push(...checkSlot(node.slot, scope, "value").diagnostics);
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
if (node.type === "each") {
|
|
1141
|
+
const { diagnostics, frame } = checkSlot(node.slot, scope, "loop");
|
|
1142
|
+
found.push(...diagnostics);
|
|
1143
|
+
checkNodes(node.children, frame ? scope.push(frame) : scope.push({ kind: "each", element: T2.unknown }), found);
|
|
1144
|
+
checkNodes(node.inverse, scope, found);
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
found.push(...checkSlot(node.slot, scope, "condition").diagnostics);
|
|
1148
|
+
checkNodes(node.children, scope, found);
|
|
1149
|
+
checkNodes(node.inverse, scope, found);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
function markerProblem(error) {
|
|
1153
|
+
if (error instanceof TemplateError) return [error.diagnostic];
|
|
1154
|
+
throw error;
|
|
1155
|
+
}
|
|
1156
|
+
function checkTextTemplate(template, env) {
|
|
1157
|
+
let nodes;
|
|
1158
|
+
try {
|
|
1159
|
+
nodes = parseTemplate(template);
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
return markerProblem(error);
|
|
1162
|
+
}
|
|
1163
|
+
const found = [];
|
|
1164
|
+
checkNodes(nodes, new CheckScope(env), found);
|
|
1165
|
+
return found;
|
|
1166
|
+
}
|
|
1167
|
+
var textDecoder2 = new TextDecoder();
|
|
1168
|
+
function checkDocxTemplate(template, env, options = {}) {
|
|
1169
|
+
const delimiters = options.cmdDelimiter ?? ["{{", "}}"];
|
|
1170
|
+
const found = [];
|
|
1171
|
+
for (const [part, bytes] of Object.entries(unzipSync2(template))) {
|
|
1172
|
+
if (!DOCX_TEMPLATE_PARTS.test(part)) continue;
|
|
1173
|
+
const xml = normalizeInlineControls(textDecoder2.decode(bytes), delimiters);
|
|
1174
|
+
const paragraphs = [...xml.matchAll(/<w:p\b[^>]*>[\s\S]*?<\/w:p>/g)];
|
|
1175
|
+
const commands = paragraphs.map((match) => commandOf(match[0], delimiters));
|
|
1176
|
+
for (const { index, message } of docxControlProblems(commands)) {
|
|
1177
|
+
found.push({ code: "markers", message, location: `${part} paragraph ${index + 1}` });
|
|
1178
|
+
}
|
|
1179
|
+
let scope = new CheckScope(env);
|
|
1180
|
+
const open = [];
|
|
1181
|
+
paragraphs.forEach((match, index) => {
|
|
1182
|
+
const location = `${part} paragraph ${index + 1}`;
|
|
1183
|
+
const locate2 = (diagnostics) => diagnostics.map((diagnostic) => ({ ...diagnostic, location }));
|
|
1184
|
+
const locateCommand = (diagnostics) => diagnostics.map(({ position: _position, ...diagnostic }) => ({ ...diagnostic, location }));
|
|
1185
|
+
const command = commands[index];
|
|
1186
|
+
if (command !== void 0) {
|
|
1187
|
+
const forMatch = command.match(/^FOR\s+(\S+)\s+IN\s+(.+)$/);
|
|
1188
|
+
if (forMatch) {
|
|
1189
|
+
const slot = expressionSlot(forMatch[2], { line: 1, column: 1 });
|
|
1190
|
+
const { diagnostics, frame } = checkSlot(slot, scope, "loop");
|
|
1191
|
+
found.push(...locateCommand(diagnostics));
|
|
1192
|
+
scope = scope.push({ ...frame ?? { element: T2.unknown }, kind: "named", name: forMatch[1] });
|
|
1193
|
+
open.push("FOR");
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
if (/^IF\s+/.test(command)) {
|
|
1197
|
+
found.push(...locateCommand(checkSlot(expressionSlot(command.slice(3), { line: 1, column: 1 }), scope, "condition").diagnostics));
|
|
1198
|
+
open.push("IF");
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
if (/^END-(?:FOR|IF)/.test(command)) {
|
|
1202
|
+
if (open.pop() === "FOR") scope = new CheckScope(scope.env, scope.frames.slice(0, -1));
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
if (command === "ELSE") return;
|
|
1206
|
+
}
|
|
1207
|
+
const text = paragraphText(match[0]);
|
|
1208
|
+
if (!text.includes(delimiters[0])) return;
|
|
1209
|
+
let nodes;
|
|
1210
|
+
try {
|
|
1211
|
+
nodes = parseTemplate(normalizeDelimiters(text, delimiters));
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
found.push(...locate2(markerProblem(error)));
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
const inner = [];
|
|
1217
|
+
checkNodes(nodes, scope, inner);
|
|
1218
|
+
found.push(...locate2(inner));
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
return found;
|
|
1222
|
+
}
|
|
1223
|
+
function commandOf(paragraph, delimiters) {
|
|
1224
|
+
const value = paragraphText(paragraph).trim();
|
|
1225
|
+
if (!value.startsWith(delimiters[0]) || !value.endsWith(delimiters[1])) return void 0;
|
|
1226
|
+
const inner = value.slice(delimiters[0].length, -delimiters[1].length).trim();
|
|
1227
|
+
return /^(?:FOR\s|IF\s|ELSE$|END-(?:FOR|IF))/.test(inner) ? inner : void 0;
|
|
1228
|
+
}
|
|
1229
|
+
function collectDirectives(nodes, found) {
|
|
1230
|
+
for (const node of nodes) {
|
|
1231
|
+
if (node.type === "text") continue;
|
|
1232
|
+
if (node.type === "value") {
|
|
1233
|
+
const ast = node.slot.ast;
|
|
1234
|
+
if (ast?.kind !== "Call" || !SIGNING_DIRECTIVES.has(ast.callee) || ast.args.length === 0) continue;
|
|
1235
|
+
const location = ast.args.at(-1);
|
|
1236
|
+
found.push({ directive: ast.callee, ...location.kind === "StringLiteral" && { location: location.value } });
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
collectDirectives(node.children, found);
|
|
1240
|
+
collectDirectives(node.inverse, found);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
function textTemplateSigningDirectives(template) {
|
|
1244
|
+
let nodes;
|
|
1245
|
+
try {
|
|
1246
|
+
nodes = parseTemplate(template);
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
if (error instanceof TemplateError) return void 0;
|
|
1249
|
+
throw error;
|
|
1250
|
+
}
|
|
1251
|
+
const found = [];
|
|
1252
|
+
collectDirectives(nodes, found);
|
|
1253
|
+
return found;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
export {
|
|
1257
|
+
TemplateError,
|
|
1258
|
+
escapeHtml,
|
|
1259
|
+
renderTemplate,
|
|
1260
|
+
requestExpressions,
|
|
1261
|
+
templateData,
|
|
1262
|
+
createTextSignatureDirectives,
|
|
1263
|
+
renderDocx,
|
|
1264
|
+
checkTextTemplate,
|
|
1265
|
+
checkDocxTemplate,
|
|
1266
|
+
textTemplateSigningDirectives
|
|
1267
|
+
};
|