@budibase/string-templates 3.2.5 → 3.2.6

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/src/index.ts DELETED
@@ -1,495 +0,0 @@
1
- import { createContext, runInNewContext } from "vm"
2
- import { create, TemplateDelegate } from "handlebars"
3
- import { registerAll, registerMinimum } from "./helpers/index"
4
- import { postprocess, preprocess } from "./processors"
5
- import {
6
- atob,
7
- btoa,
8
- FIND_ANY_HBS_REGEX,
9
- FIND_HBS_REGEX,
10
- findDoubleHbsInstances,
11
- isBackendService,
12
- prefixStrings,
13
- } from "./utilities"
14
- import { convertHBSBlock } from "./conversion"
15
- import { removeJSRunner, setJSRunner } from "./helpers/javascript"
16
-
17
- import manifest from "./manifest.json"
18
- import { ProcessOptions } from "./types"
19
- import { UserScriptError } from "./errors"
20
-
21
- export { helpersToRemoveForJs, getJsHelperList } from "./helpers/list"
22
- export { FIND_ANY_HBS_REGEX } from "./utilities"
23
- export { setJSRunner, setOnErrorLog } from "./helpers/javascript"
24
- export { iifeWrapper } from "./iife"
25
-
26
- const hbsInstance = create()
27
- registerAll(hbsInstance)
28
- const helperNames = Object.keys(hbsInstance.helpers)
29
- const hbsInstanceNoHelpers = create()
30
- registerMinimum(hbsInstanceNoHelpers)
31
- const defaultOpts: ProcessOptions = {
32
- noHelpers: false,
33
- cacheTemplates: false,
34
- noEscaping: false,
35
- escapeNewlines: false,
36
- noFinalise: false,
37
- }
38
-
39
- /**
40
- * Utility function to check if the object is valid.
41
- */
42
- function testObject(object: any) {
43
- // JSON stringify will fail if there are any cycles, stops infinite recursion
44
- try {
45
- JSON.stringify(object)
46
- } catch (err) {
47
- throw "Unable to process inputs to JSON, cannot recurse"
48
- }
49
- }
50
-
51
- function findOverlappingHelpers(context?: object) {
52
- if (!context) {
53
- return []
54
- }
55
- const contextKeys = Object.keys(context)
56
- return contextKeys.filter(key => helperNames.includes(key))
57
- }
58
-
59
- /**
60
- * Creates a HBS template function for a given string, and optionally caches it.
61
- */
62
- const templateCache: Record<string, TemplateDelegate<any>> = {}
63
- function createTemplate(
64
- string: string,
65
- opts?: ProcessOptions,
66
- context?: object
67
- ) {
68
- opts = { ...defaultOpts, ...opts }
69
- const helpersEnabled = !opts?.noHelpers
70
-
71
- // Finalising adds a helper, can't do this with no helpers
72
- const key = `${string}-${JSON.stringify(opts)}`
73
-
74
- // Reuse the cached template is possible
75
- if (opts.cacheTemplates && templateCache[key]) {
76
- return templateCache[key]
77
- }
78
-
79
- const overlappingHelpers = helpersEnabled
80
- ? findOverlappingHelpers(context)
81
- : []
82
-
83
- string = preprocess(string, {
84
- ...opts,
85
- disabledHelpers: overlappingHelpers,
86
- })
87
-
88
- if (context && helpersEnabled) {
89
- if (overlappingHelpers.length > 0) {
90
- for (const block of findHBSBlocks(string)) {
91
- string = string.replace(
92
- block,
93
- prefixStrings(block, overlappingHelpers, "./")
94
- )
95
- }
96
- }
97
- }
98
-
99
- // Optionally disable built in HBS escaping
100
- if (opts.noEscaping) {
101
- string = disableEscaping(string)
102
- }
103
-
104
- // This does not throw an error when template can't be fulfilled,
105
- // have to try correct beforehand
106
- const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
107
-
108
- const template = instance.compile(string, {
109
- strict: false,
110
- })
111
- templateCache[key] = template
112
- return template
113
- }
114
-
115
- /**
116
- * Given an input object this will recurse through all props to try and update any handlebars statements within.
117
- * @param {object|array} object The input structure which is to be recursed, it is important to note that
118
- * if the structure contains any cycles then this will fail.
119
- * @param {object} context The context that handlebars should fill data from.
120
- * @param {object|undefined} [opts] optional - specify some options for processing.
121
- * @returns {Promise<object|array>} The structure input, as fully updated as possible.
122
- */
123
- export async function processObject<T extends Record<string, any>>(
124
- object: T,
125
- context: object,
126
- opts?: ProcessOptions
127
- ): Promise<T> {
128
- testObject(object)
129
-
130
- for (const key of Object.keys(object || {})) {
131
- if (object[key] != null) {
132
- const val = object[key]
133
- let parsedValue = val
134
- if (typeof val === "string") {
135
- parsedValue = await processString(object[key], context, opts)
136
- } else if (typeof val === "object") {
137
- parsedValue = await processObject(object[key], context, opts)
138
- }
139
-
140
- // @ts-ignore
141
- object[key] = parsedValue
142
- }
143
- }
144
- return object
145
- }
146
-
147
- /**
148
- * This will process a single handlebars containing string. If the string passed in has no valid handlebars statements
149
- * then nothing will occur.
150
- * @param {string} string The template string which is the filled from the context object.
151
- * @param {object} context An object of information which will be used to enrich the string.
152
- * @param {object|undefined} [opts] optional - specify some options for processing.
153
- * @returns {Promise<string>} The enriched string, all templates should have been replaced if they can be.
154
- */
155
- export async function processString(
156
- string: string,
157
- context: object,
158
- opts?: ProcessOptions
159
- ): Promise<string> {
160
- // TODO: carry out any async calls before carrying out async call
161
- return processStringSync(string, context, opts)
162
- }
163
-
164
- /**
165
- * Given an input object this will recurse through all props to try and update any handlebars statements within. This is
166
- * a pure sync call and therefore does not have the full functionality of the async call.
167
- * @param {object|array} object The input structure which is to be recursed, it is important to note that
168
- * if the structure contains any cycles then this will fail.
169
- * @param {object} context The context that handlebars should fill data from.
170
- * @param {object|undefined} [opts] optional - specify some options for processing.
171
- * @returns {object|array} The structure input, as fully updated as possible.
172
- */
173
- export function processObjectSync(
174
- object: { [x: string]: any },
175
- context: any,
176
- opts?: ProcessOptions
177
- ): object | Array<any> {
178
- testObject(object)
179
- for (let key of Object.keys(object || {})) {
180
- let val = object[key]
181
- if (typeof val === "string") {
182
- object[key] = processStringSync(object[key], context, opts)
183
- } else if (typeof val === "object") {
184
- object[key] = processObjectSync(object[key], context, opts)
185
- }
186
- }
187
- return object
188
- }
189
-
190
- /**
191
- * This will process a single handlebars containing string. If the string passed in has no valid handlebars statements
192
- * then nothing will occur. This is a pure sync call and therefore does not have the full functionality of the async call.
193
- * @param {string} string The template string which is the filled from the context object.
194
- * @param {object} context An object of information which will be used to enrich the string.
195
- * @param {object|undefined} [opts] optional - specify some options for processing.
196
- * @returns {string} The enriched string, all templates should have been replaced if they can be.
197
- */
198
- export function processStringSync(
199
- string: string,
200
- context?: object,
201
- opts?: ProcessOptions
202
- ): string {
203
- // Take a copy of input in case of error
204
- const input = string
205
- if (typeof string !== "string") {
206
- throw "Cannot process non-string types."
207
- }
208
- function process(stringPart: string) {
209
- // context is needed to check for overlap between helpers and context
210
- const template = createTemplate(stringPart, opts, context)
211
- const now = Math.floor(Date.now() / 1000) * 1000
212
- const processedString = template({
213
- now: new Date(now).toISOString(),
214
- __opts: {
215
- ...opts,
216
- input: stringPart,
217
- },
218
- ...context,
219
- })
220
- return postprocess(processedString)
221
- }
222
- try {
223
- if (opts && opts.onlyFound) {
224
- const blocks = findHBSBlocks(string)
225
- for (let block of blocks) {
226
- const outcome = process(block)
227
- string = string.replace(block, outcome)
228
- }
229
- return string
230
- } else {
231
- return process(string)
232
- }
233
- } catch (err: any) {
234
- const { noThrow = true } = opts || {}
235
- if (noThrow) {
236
- return input
237
- }
238
- throw err
239
- }
240
- }
241
-
242
- /**
243
- * By default with expressions like {{ name }} handlebars will escape various
244
- * characters, which can be problematic. To fix this we use the syntax {{{ name }}},
245
- * this function will find any double braces and switch to triple.
246
- * @param string the string to have double HBS statements converted to triple.
247
- */
248
- export function disableEscaping(string: string) {
249
- const matches = findDoubleHbsInstances(string)
250
- if (matches == null) {
251
- return string
252
- }
253
-
254
- // find the unique set
255
- const unique = [...new Set(matches)]
256
- for (let match of unique) {
257
- // add a negative lookahead to exclude any already
258
- const regex = new RegExp(`${match}(?!})`, "g")
259
- string = string.replace(regex, `{${match}}`)
260
- }
261
- return string
262
- }
263
-
264
- /**
265
- * Simple utility function which makes sure that a templating property has been wrapped in literal specifiers correctly.
266
- * @param {string} property The property which is to be wrapped.
267
- * @returns {string} The wrapped property ready to be added to a templating string.
268
- */
269
- export function makePropSafe(property: any): string {
270
- return `[${property}]`.replace("[[", "[").replace("]]", "]")
271
- }
272
-
273
- /**
274
- * Checks whether or not a template string contains totally valid syntax (simply tries running it)
275
- * @param string The string to test for valid syntax - this may contain no templates and will be considered valid.
276
- * @param [opts] optional - specify some options for processing.
277
- * @returns {boolean} Whether or not the input string is valid.
278
- */
279
- export function isValid(string: any, opts?: any): boolean {
280
- const validCases = [
281
- "string",
282
- "number",
283
- "object",
284
- "array",
285
- "cannot read property",
286
- "undefined",
287
- "json at position 0",
288
- ]
289
- // this is a portion of a specific string always output by handlebars in the case of a syntax error
290
- const invalidCases = [`expecting '`]
291
- // don't really need a real context to check if its valid
292
- const context = {}
293
- try {
294
- const template = createTemplate(string, {
295
- ...opts,
296
- noFinalise: true,
297
- })
298
- template(context)
299
- return true
300
- } catch (err: any) {
301
- const msg = err && err.message ? err.message : err
302
- if (!msg) {
303
- return false
304
- }
305
- const invalidCase = invalidCases.some(invalidCase =>
306
- msg.toLowerCase().includes(invalidCase)
307
- )
308
- const validCase = validCases.some(validCase =>
309
- msg.toLowerCase().includes(validCase)
310
- )
311
- // special case for maths functions - don't have inputs yet
312
- return validCase && !invalidCase
313
- }
314
- }
315
-
316
- /**
317
- * We have generated a static manifest file from the helpers that this string templating package makes use of.
318
- * This manifest provides information about each of the helpers and how it can be used.
319
- * @returns The manifest JSON which has been generated from the helpers.
320
- */
321
- export function getManifest() {
322
- return manifest
323
- }
324
-
325
- /**
326
- * Checks if a HBS expression is a valid JS HBS expression
327
- * @param handlebars the HBS expression to check
328
- * @returns {boolean} whether the expression is JS or not
329
- */
330
- export function isJSBinding(handlebars: any): boolean {
331
- return decodeJSBinding(handlebars) != null
332
- }
333
-
334
- /**
335
- * Encodes a raw JS string as a JS HBS expression
336
- * @param javascript the JS code to encode
337
- * @returns {string} the JS HBS expression
338
- */
339
- export function encodeJSBinding(javascript: string): string {
340
- return `{{ js "${btoa(javascript)}" }}`
341
- }
342
-
343
- /**
344
- * Decodes a JS HBS expression to the raw JS code
345
- * @param handlebars the JS HBS expression
346
- * @returns {string|null} the raw JS code
347
- */
348
- export function decodeJSBinding(handlebars: string): string | null {
349
- if (!handlebars || typeof handlebars !== "string") {
350
- return null
351
- }
352
-
353
- // JS is only valid if it is the only HBS expression
354
- if (!handlebars.trim().startsWith("{{ js ")) {
355
- return null
356
- }
357
-
358
- const captureJSRegex = new RegExp(/{{ js "(.*)" }}/)
359
- const match = handlebars.match(captureJSRegex)
360
- if (!match || match.length < 2) {
361
- return null
362
- }
363
- return atob(match[1])
364
- }
365
-
366
- /**
367
- * Same as the doesContainString function, but will check for all the strings
368
- * before confirming it contains.
369
- * @param {string} template The template string to search.
370
- * @param {string[]} strings The strings to look for.
371
- * @returns {boolean} Will return true if all strings found in HBS statement.
372
- */
373
- export function doesContainStrings(template: string, strings: any[]): boolean {
374
- let regexp = new RegExp(FIND_HBS_REGEX)
375
- let matches = template.match(regexp)
376
- if (matches == null) {
377
- return false
378
- }
379
- for (let match of matches) {
380
- let hbs = match
381
- if (isJSBinding(match)) {
382
- hbs = decodeJSBinding(match)!
383
- }
384
- let allFound = true
385
- for (let string of strings) {
386
- if (!hbs.includes(string)) {
387
- allFound = false
388
- }
389
- }
390
- if (allFound) {
391
- return true
392
- }
393
- }
394
- return false
395
- }
396
-
397
- /**
398
- * Given a string, this will return any {{ binding }} or {{{ binding }}} type
399
- * statements.
400
- * @param {string} string The string to search within.
401
- * @return {string[]} The found HBS blocks.
402
- */
403
- export function findHBSBlocks(string: string): string[] {
404
- if (!string || typeof string !== "string") {
405
- return []
406
- }
407
- let regexp = new RegExp(FIND_ANY_HBS_REGEX)
408
- let matches = string.match(regexp)
409
- if (matches == null) {
410
- return []
411
- }
412
- return matches
413
- }
414
-
415
- /**
416
- * This function looks in the supplied template for handlebars instances, if they contain
417
- * JS the JS will be decoded and then the supplied string will be looked for. For example
418
- * if the template "Hello, your name is {{ related }}" this function would return that true
419
- * for the string "related" but not for "name" as it is not within the handlebars statement.
420
- * @param {string} template A template string to search for handlebars instances.
421
- * @param {string} string The word or sentence to search for.
422
- * @returns {boolean} The this return true if the string is found, false if not.
423
- */
424
- export function doesContainString(template: any, string: any): boolean {
425
- return doesContainStrings(template, [string])
426
- }
427
-
428
- export function convertToJS(hbs: string) {
429
- const blocks = findHBSBlocks(hbs)
430
- let js = "return `",
431
- prevBlock: string | null = null
432
- const variables: Record<string, any> = {}
433
- if (blocks.length === 0) {
434
- js += hbs
435
- }
436
- let count = 1
437
- for (let block of blocks) {
438
- let stringPart = hbs
439
- if (prevBlock) {
440
- stringPart = stringPart.split(prevBlock)[1]
441
- }
442
- stringPart = stringPart.split(block)[0]
443
- prevBlock = block
444
- const { variable, value } = convertHBSBlock(block, count++)
445
- variables[variable] = value
446
- js += `${[stringPart]}\${${variable}}`
447
- }
448
- let varBlock = ""
449
- for (let [variable, value] of Object.entries(variables)) {
450
- varBlock += `const ${variable} = ${value};\n`
451
- }
452
- js += "`;"
453
- return `${varBlock}${js}`
454
- }
455
-
456
- export { JsTimeoutError, UserScriptError } from "./errors"
457
-
458
- export function browserJSSetup() {
459
- /**
460
- * Use polyfilled vm to run JS scripts in a browser Env
461
- */
462
- setJSRunner((js: string, context: Record<string, any>) => {
463
- createContext(context)
464
-
465
- const wrappedJs = `
466
- result = {
467
- result: null,
468
- error: null,
469
- };
470
-
471
- try {
472
- result.result = ${js};
473
- } catch (e) {
474
- result.error = e;
475
- }
476
-
477
- result;
478
- `
479
-
480
- const result = runInNewContext(wrappedJs, context, { timeout: 1000 })
481
- if (result.error) {
482
- throw new UserScriptError(result.error)
483
- }
484
- return result.result
485
- })
486
- }
487
-
488
- export function defaultJSSetup() {
489
- if (!isBackendService()) {
490
- browserJSSetup()
491
- } else {
492
- removeJSRunner()
493
- }
494
- }
495
- defaultJSSetup()