@budibase/string-templates 3.2.4 → 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/dist/bundle.mjs +1 -1
- package/dist/iife.mjs +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -8
- package/src/conversion/index.ts +0 -131
- package/src/errors.ts +0 -20
- package/src/helpers/Helper.ts +0 -36
- package/src/helpers/constants.ts +0 -41
- package/src/helpers/date.ts +0 -133
- package/src/helpers/external.ts +0 -57
- package/src/helpers/index.ts +0 -103
- package/src/helpers/javascript.ts +0 -168
- package/src/helpers/list.ts +0 -72
- package/src/iife.ts +0 -3
- package/src/index.ts +0 -495
- package/src/manifest.json +0 -1364
- package/src/processors/index.ts +0 -37
- package/src/processors/postprocessor.ts +0 -57
- package/src/processors/preprocessor.ts +0 -90
- package/src/types.ts +0 -10
- package/src/utilities.ts +0 -88
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()
|