@budibase/string-templates 3.2.4 → 3.2.6

Sign up to get free protection for your applications and to get access to all the features.
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()