@dev-crew-berlin/enter-js-utils 0.98.11 → 0.99.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/dist/lib/email-handlebars.d.ts +31 -0
- package/dist/lib/email-handlebars.js +436 -0
- package/dist/lib/email-handlebars.test.js +339 -0
- package/dist/lib/email-links.d.ts +46 -0
- package/dist/lib/email-links.js +68 -0
- package/dist/lib/email-links.test.js +348 -0
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.js +3 -1
- package/dist/lib/tokens.d.ts +20 -0
- package/dist/lib/tokens.js +48 -0
- package/dist/lib/tokens.test.js +508 -0
- package/dist/ui/companion-info.d.ts +3 -0
- package/dist/ui/companion-info.js +24 -8
- package/dist/ui/companion-info.stories.js +11 -0
- package/dist/ui/form-elements/input.d.ts +1 -1
- package/dist/ui/form-elements/select.d.ts +1 -1
- package/dist/ui/form-elements/select.js +4 -4
- package/dist/ui/tag.js +3 -3
- package/package.json +2 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { LinkBuilderOptions } from './email-links';
|
|
2
|
+
/**
|
|
3
|
+
* A from-scratch TS port of the pybars helper set registered in
|
|
4
|
+
* app/templates.py (enter-core) -- the "mechanical, data-only" ones the
|
|
5
|
+
* plan calls out for reuse, plus the link builders (wired to the shared
|
|
6
|
+
* TS builders in email-links.ts instead of a backend round trip, since
|
|
7
|
+
* tracking links in particular are template-dependent: an author can wrap
|
|
8
|
+
* an arbitrary one-off URL anywhere in a block, so the set of links
|
|
9
|
+
* needing signing can't be enumerated ahead of render time).
|
|
10
|
+
*
|
|
11
|
+
* Each helper's behaviour is checked against a characterization test of
|
|
12
|
+
* the *actual* pybars implementation in
|
|
13
|
+
* enter-core/tests/unittests/app/render_templates_test.py -- see
|
|
14
|
+
* email-handlebars.test.ts for the mirrored cases, including two
|
|
15
|
+
* intentional divergences documented inline below (`escape`, `if`'s `!=`).
|
|
16
|
+
*/
|
|
17
|
+
export type EmailLinkContext = {
|
|
18
|
+
attendeeId: string;
|
|
19
|
+
emailName: string;
|
|
20
|
+
mailingId?: string;
|
|
21
|
+
trackingEnabled: boolean;
|
|
22
|
+
};
|
|
23
|
+
export type PartialResolver = (name: string) => Promise<string>;
|
|
24
|
+
export type RenderHandlebarsOptions = {
|
|
25
|
+
lang: string;
|
|
26
|
+
languages: string[];
|
|
27
|
+
linkOptions: LinkBuilderOptions;
|
|
28
|
+
linkContext: EmailLinkContext;
|
|
29
|
+
resolvePartial: PartialResolver;
|
|
30
|
+
};
|
|
31
|
+
export declare function renderHandlebars(source: string, data: Record<string, unknown>, options: RenderHandlebarsOptions): Promise<string>;
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import Handlebars from 'handlebars';
|
|
2
|
+
import { makeGoogleWalletLink, makeQrCodeLink, makeRegistrationLink, makeTrackingLink, makeUnsubscribeLink, makeWalletLink } from './email-links';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A from-scratch TS port of the pybars helper set registered in
|
|
6
|
+
* app/templates.py (enter-core) -- the "mechanical, data-only" ones the
|
|
7
|
+
* plan calls out for reuse, plus the link builders (wired to the shared
|
|
8
|
+
* TS builders in email-links.ts instead of a backend round trip, since
|
|
9
|
+
* tracking links in particular are template-dependent: an author can wrap
|
|
10
|
+
* an arbitrary one-off URL anywhere in a block, so the set of links
|
|
11
|
+
* needing signing can't be enumerated ahead of render time).
|
|
12
|
+
*
|
|
13
|
+
* Each helper's behaviour is checked against a characterization test of
|
|
14
|
+
* the *actual* pybars implementation in
|
|
15
|
+
* enter-core/tests/unittests/app/render_templates_test.py -- see
|
|
16
|
+
* email-handlebars.test.ts for the mirrored cases, including two
|
|
17
|
+
* intentional divergences documented inline below (`escape`, `if`'s `!=`).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
function translate(item, lang) {
|
|
21
|
+
if (typeof item === 'string') return item;
|
|
22
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
23
|
+
return item[lang];
|
|
24
|
+
}
|
|
25
|
+
return item;
|
|
26
|
+
}
|
|
27
|
+
function stringifyTranslated(item, lang) {
|
|
28
|
+
const value = translate(item, lang);
|
|
29
|
+
return value == null ? '' : String(value);
|
|
30
|
+
}
|
|
31
|
+
function helperArgs(args) {
|
|
32
|
+
return {
|
|
33
|
+
options: args[args.length - 1],
|
|
34
|
+
explicit: args.slice(0, -1)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function compareValues(op, left, right) {
|
|
38
|
+
try {
|
|
39
|
+
switch (op) {
|
|
40
|
+
case '==':
|
|
41
|
+
return left === right;
|
|
42
|
+
// mirrors a bug in the legacy pybars `if` helper where != behaves
|
|
43
|
+
// exactly like == -- documented, not "fixed", so templates ported
|
|
44
|
+
// from the legacy renderer keep behaving the same way here
|
|
45
|
+
case '!=':
|
|
46
|
+
return left === right;
|
|
47
|
+
case '>':
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
return left > right;
|
|
50
|
+
case '<':
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
+
return left < right;
|
|
53
|
+
case '>=':
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
+
return left >= right;
|
|
56
|
+
case '<=':
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
+
return left <= right;
|
|
59
|
+
case 'in':
|
|
60
|
+
if (typeof right === 'string' || Array.isArray(right)) {
|
|
61
|
+
return right.includes(left);
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
default:
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function decimalPlaces(value) {
|
|
72
|
+
const s = String(value);
|
|
73
|
+
return s.includes('.') ? s.split('.')[1].length : 0;
|
|
74
|
+
}
|
|
75
|
+
function roundHalfToEven(value, decimals) {
|
|
76
|
+
const factor = 10 ** decimals;
|
|
77
|
+
const scaled = value * factor;
|
|
78
|
+
if (Math.abs(scaled - Math.trunc(scaled) - 0.5) < 1e-9) {
|
|
79
|
+
const floor = Math.floor(scaled);
|
|
80
|
+
return (floor % 2 === 0 ? floor : floor + 1) / factor;
|
|
81
|
+
}
|
|
82
|
+
return Math.round(scaled) / factor;
|
|
83
|
+
}
|
|
84
|
+
function formatCalcResult(value) {
|
|
85
|
+
// Python's round() always returns a float, and str(float) always shows
|
|
86
|
+
// at least one decimal digit (2.0, not 2) -- JS's default number ->
|
|
87
|
+
// string conversion drops it, so whole numbers need it added back.
|
|
88
|
+
return Number.isInteger(value) ? `${value}.0` : String(value);
|
|
89
|
+
}
|
|
90
|
+
function parseDayFirst(value) {
|
|
91
|
+
if (value instanceof Date) return value;
|
|
92
|
+
const s = String(value);
|
|
93
|
+
// "DD.MM.YYYY HH:mm" -- the format legacy templates author literal
|
|
94
|
+
// comparison timestamps in; anything else is handed to Date() as-is
|
|
95
|
+
// (covers ISO strings, which is what real attendee/event data uses).
|
|
96
|
+
const match = /^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2}))?/.exec(s);
|
|
97
|
+
if (match) {
|
|
98
|
+
const [, day, month, year, hour, minute] = match;
|
|
99
|
+
return new Date(Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour ?? 0), Number(minute ?? 0)));
|
|
100
|
+
}
|
|
101
|
+
return new Date(s);
|
|
102
|
+
}
|
|
103
|
+
function sizeofFmt(value) {
|
|
104
|
+
let num = Number(value);
|
|
105
|
+
if (!Number.isFinite(num)) num = 0;
|
|
106
|
+
if (num < 1024) return `${Math.trunc(num)} B`;
|
|
107
|
+
for (const unit of ['KB', 'MB', 'GB']) {
|
|
108
|
+
num /= 1024;
|
|
109
|
+
if (Math.abs(num) < 100) return `${num.toFixed(1)} ${unit}`;
|
|
110
|
+
if (Math.abs(num) < 1024) return `${Math.trunc(num)} ${unit}`;
|
|
111
|
+
}
|
|
112
|
+
return `${num.toFixed(1)} TB`;
|
|
113
|
+
}
|
|
114
|
+
function registerDataHelpers(hb, lang, languages) {
|
|
115
|
+
hb.registerHelper('_', function (value, ...rest) {
|
|
116
|
+
const explicit = rest.slice(0, -1);
|
|
117
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
118
|
+
const key = explicit.length > 0 ? String(explicit[0]) : lang;
|
|
119
|
+
return value[key];
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
});
|
|
123
|
+
hb.registerHelper('upper', function (value, ...rest) {
|
|
124
|
+
const explicit = rest.slice(0, -1);
|
|
125
|
+
const key = explicit.length > 0 ? String(explicit[0]) : lang;
|
|
126
|
+
const translated = value && typeof value === 'object' && !Array.isArray(value) ? value[key] : value;
|
|
127
|
+
return translated == null ? '' : String(translated).toUpperCase();
|
|
128
|
+
});
|
|
129
|
+
hb.registerHelper('lower', function (value, ...rest) {
|
|
130
|
+
const explicit = rest.slice(0, -1);
|
|
131
|
+
const key = explicit.length > 0 ? String(explicit[0]) : lang;
|
|
132
|
+
const translated = value && typeof value === 'object' && !Array.isArray(value) ? value[key] : value;
|
|
133
|
+
return translated == null ? '' : String(translated).toLowerCase();
|
|
134
|
+
});
|
|
135
|
+
for (const targetLang of languages) {
|
|
136
|
+
hb.registerHelper(targetLang, function (...args) {
|
|
137
|
+
const {
|
|
138
|
+
options,
|
|
139
|
+
explicit
|
|
140
|
+
} = helperArgs(args);
|
|
141
|
+
const matches = targetLang === lang;
|
|
142
|
+
if (typeof options.fn === 'function') {
|
|
143
|
+
return matches ? options.fn(this) : options.inverse(this);
|
|
144
|
+
}
|
|
145
|
+
if (!matches) return undefined;
|
|
146
|
+
return explicit.map(a => stringifyTranslated(a, lang)).join('');
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
hb.registerHelper('eq', function (...args) {
|
|
150
|
+
const {
|
|
151
|
+
options,
|
|
152
|
+
explicit
|
|
153
|
+
} = helperArgs(args);
|
|
154
|
+
if (explicit.length >= 2) {
|
|
155
|
+
const left = translate(explicit[0], lang);
|
|
156
|
+
for (const arg of explicit.slice(1)) {
|
|
157
|
+
if (left === translate(arg, lang)) return options.fn(this);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return options.inverse(this);
|
|
161
|
+
});
|
|
162
|
+
hb.registerHelper('if', function (...args) {
|
|
163
|
+
const {
|
|
164
|
+
options,
|
|
165
|
+
explicit
|
|
166
|
+
} = helperArgs(args);
|
|
167
|
+
if (explicit.length >= 3) {
|
|
168
|
+
const operator = String(explicit[1]);
|
|
169
|
+
const left = translate(explicit[0], lang);
|
|
170
|
+
for (const arg of explicit.slice(2)) {
|
|
171
|
+
if (compareValues(operator, left, translate(arg, lang))) return options.fn(this);
|
|
172
|
+
}
|
|
173
|
+
} else if (explicit.length === 1) {
|
|
174
|
+
if (explicit[0]) return options.fn(this);
|
|
175
|
+
}
|
|
176
|
+
return options.inverse(this);
|
|
177
|
+
});
|
|
178
|
+
hb.registerHelper('if_in', function (...args) {
|
|
179
|
+
const {
|
|
180
|
+
options,
|
|
181
|
+
explicit
|
|
182
|
+
} = helperArgs(args);
|
|
183
|
+
const haystack = explicit[0];
|
|
184
|
+
if (explicit.length >= 2 && (Array.isArray(haystack) || typeof haystack === 'string')) {
|
|
185
|
+
for (const arg of explicit.slice(1)) {
|
|
186
|
+
const needle = translate(arg, lang);
|
|
187
|
+
if (haystack.includes(needle)) return options.fn(this);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return options.inverse(this);
|
|
191
|
+
});
|
|
192
|
+
hb.registerHelper('join', function (...args) {
|
|
193
|
+
const {
|
|
194
|
+
explicit
|
|
195
|
+
} = helperArgs(args);
|
|
196
|
+
const list = explicit[0];
|
|
197
|
+
if (!Array.isArray(list)) return '';
|
|
198
|
+
if (explicit.length === 2) {
|
|
199
|
+
return list.join(stringifyTranslated(explicit[1], lang));
|
|
200
|
+
}
|
|
201
|
+
if (explicit.length === 3 && list.length) {
|
|
202
|
+
if (list.length === 1) return list[0];
|
|
203
|
+
const joiner = stringifyTranslated(explicit[1], lang);
|
|
204
|
+
return list.slice(0, -1).join(joiner) + String(explicit[2]) + list[list.length - 1];
|
|
205
|
+
}
|
|
206
|
+
return '';
|
|
207
|
+
});
|
|
208
|
+
hb.registerHelper('calc', function (...args) {
|
|
209
|
+
const {
|
|
210
|
+
explicit
|
|
211
|
+
} = helperArgs(args);
|
|
212
|
+
if (explicit.length !== 3) return '';
|
|
213
|
+
const a = Number(explicit[0]);
|
|
214
|
+
const b = Number(explicit[2]);
|
|
215
|
+
if (Number.isNaN(a) || Number.isNaN(b)) return '';
|
|
216
|
+
const maxDecimals = Math.max(decimalPlaces(explicit[0]), decimalPlaces(explicit[2]));
|
|
217
|
+
let result = null;
|
|
218
|
+
switch (explicit[1]) {
|
|
219
|
+
case '+':
|
|
220
|
+
result = a + b;
|
|
221
|
+
break;
|
|
222
|
+
case '-':
|
|
223
|
+
result = a - b;
|
|
224
|
+
break;
|
|
225
|
+
case '*':
|
|
226
|
+
result = a * b;
|
|
227
|
+
break;
|
|
228
|
+
case '/':
|
|
229
|
+
result = a / b;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
if (result === null) return '';
|
|
233
|
+
return formatCalcResult(roundHalfToEven(result, maxDecimals));
|
|
234
|
+
});
|
|
235
|
+
hb.registerHelper('count', function (value) {
|
|
236
|
+
if (Array.isArray(value) || typeof value === 'string') return value.length;
|
|
237
|
+
if (value && typeof value === 'object') return Object.keys(value).length;
|
|
238
|
+
return '';
|
|
239
|
+
});
|
|
240
|
+
hb.registerHelper('get', function (...args) {
|
|
241
|
+
const {
|
|
242
|
+
options,
|
|
243
|
+
explicit
|
|
244
|
+
} = helperArgs(args);
|
|
245
|
+
const obj = explicit[0];
|
|
246
|
+
const key = explicit[1];
|
|
247
|
+
const value = obj != null ? obj[key] : undefined;
|
|
248
|
+
const fallback = explicit.length > 2 ? explicit[2] : '';
|
|
249
|
+
if (typeof options.fn === 'function') {
|
|
250
|
+
return options.fn(value !== undefined ? value : fallback);
|
|
251
|
+
}
|
|
252
|
+
return value !== undefined ? value : fallback;
|
|
253
|
+
});
|
|
254
|
+
hb.registerHelper('range', function (...args) {
|
|
255
|
+
const {
|
|
256
|
+
options,
|
|
257
|
+
explicit
|
|
258
|
+
} = helperArgs(args);
|
|
259
|
+
const nums = explicit.map(Number);
|
|
260
|
+
const [start, stop, step] = nums.length === 1 ? [0, nums[0], 1] : nums.length === 2 ? [nums[0], nums[1], 1] : nums;
|
|
261
|
+
let result = '';
|
|
262
|
+
for (let x = start; step > 0 ? x < stop : x > stop; x += step) {
|
|
263
|
+
result += options.fn(x);
|
|
264
|
+
}
|
|
265
|
+
return result;
|
|
266
|
+
});
|
|
267
|
+
hb.registerHelper('size', function (value) {
|
|
268
|
+
return sizeofFmt(value);
|
|
269
|
+
});
|
|
270
|
+
hb.registerHelper('escape', function (value) {
|
|
271
|
+
// Deliberate deviation from the legacy pybars `escape` helper, which
|
|
272
|
+
// returns str(x).encode(...) -- a bytes object -- and ends up
|
|
273
|
+
// rendering its own Python repr (e.g. "b'<b>'") once pybars
|
|
274
|
+
// stringifies it. That's almost certainly an unintentional bug no
|
|
275
|
+
// template actually relies on; this does the HTML-escaping the
|
|
276
|
+
// helper's name promises instead of reproducing the bug.
|
|
277
|
+
return new Handlebars.SafeString(Handlebars.escapeExpression(String(value ?? '')));
|
|
278
|
+
});
|
|
279
|
+
function dateComparisonHelper(mode) {
|
|
280
|
+
return function (...args) {
|
|
281
|
+
const {
|
|
282
|
+
options,
|
|
283
|
+
explicit
|
|
284
|
+
} = helperArgs(args);
|
|
285
|
+
const forcedIndex = mode === 'between' ? 2 : 1;
|
|
286
|
+
if (explicit.length > forcedIndex && explicit[forcedIndex]) return options.fn(this);
|
|
287
|
+
if (mode === 'between') {
|
|
288
|
+
if (explicit.length < 2) return options.inverse(this);
|
|
289
|
+
const a = parseDayFirst(explicit[0]).getTime();
|
|
290
|
+
const b = parseDayFirst(explicit[1]).getTime();
|
|
291
|
+
const now = Date.now();
|
|
292
|
+
const inside = a < now && now < b || b < now && now < a;
|
|
293
|
+
return inside ? options.fn(this) : options.inverse(this);
|
|
294
|
+
}
|
|
295
|
+
if (explicit.length === 0) return options.inverse(this);
|
|
296
|
+
const timestamp = parseDayFirst(explicit[0]).getTime();
|
|
297
|
+
const now = Date.now();
|
|
298
|
+
const passed = mode === 'before' ? now < timestamp : now > timestamp;
|
|
299
|
+
return passed ? options.fn(this) : options.inverse(this);
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
hb.registerHelper('before', dateComparisonHelper('before'));
|
|
303
|
+
hb.registerHelper('after', dateComparisonHelper('after'));
|
|
304
|
+
hb.registerHelper('between', dateComparisonHelper('between'));
|
|
305
|
+
}
|
|
306
|
+
function registerDateHelpers(hb, lang) {
|
|
307
|
+
const dateFormat = (style, timeZone = 'UTC') => function (value) {
|
|
308
|
+
const date = value instanceof Date ? value : new Date(String(value));
|
|
309
|
+
return new Intl.DateTimeFormat(lang, {
|
|
310
|
+
dateStyle: style,
|
|
311
|
+
timeZone
|
|
312
|
+
}).format(date);
|
|
313
|
+
};
|
|
314
|
+
hb.registerHelper('shortdate', dateFormat('short'));
|
|
315
|
+
hb.registerHelper('date', dateFormat('medium'));
|
|
316
|
+
hb.registerHelper('longdate', dateFormat('long'));
|
|
317
|
+
hb.registerHelper('fulldate', dateFormat('full'));
|
|
318
|
+
hb.registerHelper('time', function (value) {
|
|
319
|
+
const date = value instanceof Date ? value : new Date(String(value));
|
|
320
|
+
return new Intl.DateTimeFormat(lang, {
|
|
321
|
+
timeStyle: 'short',
|
|
322
|
+
timeZone: 'Europe/Berlin'
|
|
323
|
+
}).format(date);
|
|
324
|
+
});
|
|
325
|
+
hb.registerHelper('longdateUtc', function (value) {
|
|
326
|
+
const date = value instanceof Date ? value : new Date(String(value));
|
|
327
|
+
return new Intl.DateTimeFormat(lang, {
|
|
328
|
+
dateStyle: 'long',
|
|
329
|
+
timeZone: 'Europe/Berlin'
|
|
330
|
+
}).format(date);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Handlebars.js HTML-escapes every helper return value by default (e.g. "="
|
|
335
|
+
// becomes "="), which corrupts any URL with a query string --
|
|
336
|
+
// "?token=..." becomes "?token=...". pybars does not do this to
|
|
337
|
+
// helper-produced strings, so every link helper below needs to opt out via
|
|
338
|
+
// SafeString to match legacy template behaviour (confirmed against a real
|
|
339
|
+
// pybars render in enter-core/tests/unittests/app/render_templates_test.py).
|
|
340
|
+
function safe(value) {
|
|
341
|
+
return new Handlebars.SafeString(value);
|
|
342
|
+
}
|
|
343
|
+
function registerLinkHelpers(hb, linkOptions, ctx) {
|
|
344
|
+
hb.registerHelper('link', function (path) {
|
|
345
|
+
return safe(makeTrackingLink(linkOptions, {
|
|
346
|
+
attendeeId: ctx.attendeeId,
|
|
347
|
+
emailName: ctx.emailName,
|
|
348
|
+
mailingId: ctx.mailingId,
|
|
349
|
+
urlLink: path,
|
|
350
|
+
trackingEnabled: ctx.trackingEnabled
|
|
351
|
+
}));
|
|
352
|
+
});
|
|
353
|
+
hb.registerHelper('unsubscribe_link', function (emailAddress) {
|
|
354
|
+
return safe(makeUnsubscribeLink(linkOptions, {
|
|
355
|
+
attendeeId: ctx.attendeeId,
|
|
356
|
+
emailName: ctx.emailName,
|
|
357
|
+
emailAddress
|
|
358
|
+
}));
|
|
359
|
+
});
|
|
360
|
+
hb.registerHelper('registration_link', function (...args) {
|
|
361
|
+
const {
|
|
362
|
+
explicit
|
|
363
|
+
} = helperArgs(args);
|
|
364
|
+
const id = explicit[0] ?? ctx.attendeeId;
|
|
365
|
+
const sessionType = explicit[1] ?? 'register';
|
|
366
|
+
const daysValid = explicit[2] != null ? Number(explicit[2]) : 15;
|
|
367
|
+
return safe(makeRegistrationLink(linkOptions, {
|
|
368
|
+
attendeeId: id,
|
|
369
|
+
sessionType,
|
|
370
|
+
expiresInSeconds: daysValid * 24 * 60 * 60,
|
|
371
|
+
email: ctx.mailingId ? {
|
|
372
|
+
name: ctx.emailName,
|
|
373
|
+
mailingId: ctx.mailingId
|
|
374
|
+
} : undefined
|
|
375
|
+
}));
|
|
376
|
+
});
|
|
377
|
+
hb.registerHelper('wallet_link', function (id) {
|
|
378
|
+
const link = makeWalletLink(linkOptions, id ?? ctx.attendeeId);
|
|
379
|
+
// mirrors the legacy helper: the wallet link itself is wrapped in a
|
|
380
|
+
// tracked link, unlike google_wallet_link below
|
|
381
|
+
return safe(makeTrackingLink(linkOptions, {
|
|
382
|
+
attendeeId: ctx.attendeeId,
|
|
383
|
+
emailName: ctx.emailName,
|
|
384
|
+
mailingId: ctx.mailingId,
|
|
385
|
+
urlLink: link,
|
|
386
|
+
trackingEnabled: ctx.trackingEnabled
|
|
387
|
+
}));
|
|
388
|
+
});
|
|
389
|
+
hb.registerHelper('google_wallet_link', function (id) {
|
|
390
|
+
return safe(makeGoogleWalletLink(linkOptions, id ?? ctx.attendeeId));
|
|
391
|
+
});
|
|
392
|
+
hb.registerHelper('qr_code_link', function (...args) {
|
|
393
|
+
const {
|
|
394
|
+
explicit
|
|
395
|
+
} = helperArgs(args);
|
|
396
|
+
return safe(makeQrCodeLink(linkOptions, {
|
|
397
|
+
content: String(explicit[0]),
|
|
398
|
+
size: explicit[1] != null ? Number(explicit[1]) : undefined,
|
|
399
|
+
background: explicit[2],
|
|
400
|
+
body: explicit[3],
|
|
401
|
+
corners: explicit[4],
|
|
402
|
+
fancy: explicit[5] != null ? Number(explicit[5]) : undefined
|
|
403
|
+
}));
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
const PARTIAL_REFERENCE_PATTERN = /\{\{>\s*([a-zA-Z0-9_-]+)/g;
|
|
407
|
+
const MAX_PARTIAL_DEPTH = 10;
|
|
408
|
+
function extractPartialNames(source) {
|
|
409
|
+
return [...source.matchAll(PARTIAL_REFERENCE_PATTERN)].map(m => m[1]);
|
|
410
|
+
}
|
|
411
|
+
async function resolvePartialsRecursively(hb, source, resolvePartial, seen, depth) {
|
|
412
|
+
if (depth > MAX_PARTIAL_DEPTH) return;
|
|
413
|
+
for (const name of extractPartialNames(source)) {
|
|
414
|
+
if (seen.has(name)) continue;
|
|
415
|
+
seen.add(name);
|
|
416
|
+
let partialSource;
|
|
417
|
+
try {
|
|
418
|
+
partialSource = await resolvePartial(name);
|
|
419
|
+
} catch {
|
|
420
|
+
// mirrors get_template()'s behaviour of never raising -- an
|
|
421
|
+
// unresolvable partial silently becomes a literal placeholder
|
|
422
|
+
partialSource = 'Template not found';
|
|
423
|
+
}
|
|
424
|
+
hb.registerPartial(name, partialSource);
|
|
425
|
+
await resolvePartialsRecursively(hb, partialSource, resolvePartial, seen, depth + 1);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
export async function renderHandlebars(source, data, options) {
|
|
429
|
+
const hb = Handlebars.create();
|
|
430
|
+
registerDataHelpers(hb, options.lang, options.languages);
|
|
431
|
+
registerDateHelpers(hb, options.lang);
|
|
432
|
+
registerLinkHelpers(hb, options.linkOptions, options.linkContext);
|
|
433
|
+
await resolvePartialsRecursively(hb, source, options.resolvePartial, new Set(), 0);
|
|
434
|
+
const template = hb.compile(source);
|
|
435
|
+
return template(data);
|
|
436
|
+
}
|