forked from tsai/budibase
9 changed files with 289 additions and 258 deletions
@ -1,169 +1,28 @@ |
|||
const handlebars = require("handlebars") |
|||
const { registerAll } = require("./helpers/index") |
|||
const processors = require("./processors") |
|||
const { removeHandlebarsStatements } = require("./utilities") |
|||
const manifest = require("../manifest.json") |
|||
const JS = require("./helpers/javascript") |
|||
|
|||
const hbsInstance = handlebars.create() |
|||
registerAll(hbsInstance) |
|||
|
|||
/** |
|||
* utility function to check if the object is valid |
|||
*/ |
|||
function testObject(object) { |
|||
// JSON stringify will fail if there are any cycles, stops infinite recursion |
|||
try { |
|||
JSON.stringify(object) |
|||
} catch (err) { |
|||
throw "Unable to process inputs to JSON, cannot recurse" |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Given an input object this will recurse through all props to try and update any handlebars statements within. |
|||
* @param {object|array} object The input structure which is to be recursed, it is important to note that |
|||
* if the structure contains any cycles then this will fail. |
|||
* @param {object} context The context that handlebars should fill data from. |
|||
* @returns {Promise<object|array>} The structure input, as fully updated as possible. |
|||
*/ |
|||
module.exports.processObject = async (object, context) => { |
|||
testObject(object) |
|||
for (let key of Object.keys(object || {})) { |
|||
if (object[key] != null) { |
|||
let val = object[key] |
|||
if (typeof val === "string") { |
|||
object[key] = await module.exports.processString(object[key], context) |
|||
} else if (typeof val === "object") { |
|||
object[key] = await module.exports.processObject(object[key], context) |
|||
} |
|||
} |
|||
} |
|||
return object |
|||
} |
|||
|
|||
/** |
|||
* This will process a single handlebars containing string. If the string passed in has no valid handlebars statements |
|||
* then nothing will occur. |
|||
* @param {string} string The template string which is the filled from the context object. |
|||
* @param {object} context An object of information which will be used to enrich the string. |
|||
* @returns {Promise<string>} The enriched string, all templates should have been replaced if they can be. |
|||
*/ |
|||
module.exports.processString = async (string, context) => { |
|||
// TODO: carry out any async calls before carrying out async call |
|||
return module.exports.processStringSync(string, context) |
|||
} |
|||
|
|||
/** |
|||
* Given an input object this will recurse through all props to try and update any handlebars statements within. This is |
|||
* a pure sync call and therefore does not have the full functionality of the async call. |
|||
* @param {object|array} object The input structure which is to be recursed, it is important to note that |
|||
* if the structure contains any cycles then this will fail. |
|||
* @param {object} context The context that handlebars should fill data from. |
|||
* @returns {object|array} The structure input, as fully updated as possible. |
|||
*/ |
|||
module.exports.processObjectSync = (object, context) => { |
|||
testObject(object) |
|||
for (let key of Object.keys(object || {})) { |
|||
let val = object[key] |
|||
if (typeof val === "string") { |
|||
object[key] = module.exports.processStringSync(object[key], context) |
|||
} else if (typeof val === "object") { |
|||
object[key] = module.exports.processObjectSync(object[key], context) |
|||
} |
|||
} |
|||
return object |
|||
} |
|||
|
|||
/** |
|||
* This will process a single handlebars containing string. If the string passed in has no valid handlebars statements |
|||
* then nothing will occur. This is a pure sync call and therefore does not have the full functionality of the async call. |
|||
* @param {string} string The template string which is the filled from the context object. |
|||
* @param {object} context An object of information which will be used to enrich the string. |
|||
* @returns {string} The enriched string, all templates should have been replaced if they can be. |
|||
*/ |
|||
module.exports.processStringSync = (string, context) => { |
|||
if (!exports.isValid(string)) { |
|||
return string |
|||
} |
|||
// take a copy of input incase error |
|||
const input = string |
|||
if (typeof string !== "string") { |
|||
throw "Cannot process non-string types." |
|||
} |
|||
try { |
|||
string = processors.preprocess(string) |
|||
// this does not throw an error when template can't be fulfilled, have to try correct beforehand |
|||
const template = hbsInstance.compile(string, { |
|||
strict: false, |
|||
}) |
|||
return processors.postprocess(template({ |
|||
now: new Date().toISOString(), |
|||
...context, |
|||
})) |
|||
} catch (err) { |
|||
return removeHandlebarsStatements(input) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Simple utility function which makes sure that a templating property has been wrapped in literal specifiers correctly. |
|||
* @param {string} property The property which is to be wrapped. |
|||
* @returns {string} The wrapped property ready to be added to a templating string. |
|||
*/ |
|||
module.exports.makePropSafe = property => { |
|||
return `[${property}]`.replace("[[", "[").replace("]]", "]") |
|||
} |
|||
|
|||
/** |
|||
* Checks whether or not a template string contains totally valid syntax (simply tries running it) |
|||
* @param string The string to test for valid syntax - this may contain no templates and will be considered valid. |
|||
* @returns {boolean} Whether or not the input string is valid. |
|||
*/ |
|||
module.exports.isValid = string => { |
|||
const validCases = [ |
|||
"string", |
|||
"number", |
|||
"object", |
|||
"array", |
|||
"cannot read property", |
|||
"undefined", |
|||
] |
|||
// this is a portion of a specific string always output by handlebars in the case of a syntax error |
|||
const invalidCases = [`expecting '`] |
|||
// don't really need a real context to check if its valid |
|||
const context = {} |
|||
try { |
|||
hbsInstance.compile(processors.preprocess(string, false))(context) |
|||
return true |
|||
} catch (err) { |
|||
const msg = err && err.message ? err.message : err |
|||
if (!msg) { |
|||
return false |
|||
} |
|||
const invalidCase = invalidCases.some(invalidCase => |
|||
msg.toLowerCase().includes(invalidCase) |
|||
) |
|||
const validCase = validCases.some(validCase => |
|||
msg.toLowerCase().includes(validCase) |
|||
) |
|||
// special case for maths functions - don't have inputs yet |
|||
return validCase && !invalidCase |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* We have generated a static manifest file from the helpers that this string templating package makes use of. |
|||
* This manifest provides information about each of the helpers and how it can be used. |
|||
* @returns The manifest JSON which has been generated from the helpers. |
|||
*/ |
|||
module.exports.getManifest = () => { |
|||
return manifest |
|||
} |
|||
|
|||
/** |
|||
* Export utilities for working with JS bindings |
|||
*/ |
|||
module.exports.isJSBinding = JS.isJSBinding |
|||
module.exports.decodeJSBinding = JS.decodeJSBinding |
|||
module.exports.encodeJSBinding = JS.encodeJSBinding |
|||
const { VM } = require("vm2") |
|||
const templates = require("./index.js") |
|||
const { setJSRunner } = require("./helpers/javascript") |
|||
|
|||
/** |
|||
* CJS entrypoint for rollup |
|||
*/ |
|||
module.exports.isValid = templates.isValid |
|||
module.exports.makePropSafe = templates.makePropSafe |
|||
module.exports.getManifest = templates.getManifest |
|||
module.exports.isJSBinding = templates.isJSBinding |
|||
module.exports.encodeJSBinding = templates.encodeJSBinding |
|||
module.exports.decodeJSBinding = templates.decodeJSBinding |
|||
module.exports.processStringSync = templates.processStringSync |
|||
module.exports.processObjectSync = templates.processObjectSync |
|||
module.exports.processString = templates.processString |
|||
module.exports.processObject = templates.processObject |
|||
|
|||
/** |
|||
* Use vm2 to run JS scripts in a node env |
|||
*/ |
|||
setJSRunner((js, context) => { |
|||
const vm = new VM({ |
|||
sandbox: context, |
|||
timeout: 1000 |
|||
}) |
|||
return vm.run(js) |
|||
}) |
|||
@ -0,0 +1,204 @@ |
|||
const handlebars = require("handlebars") |
|||
const { registerAll } = require("./helpers/index") |
|||
const processors = require("./processors") |
|||
const { removeHandlebarsStatements, atob, btoa } = require("./utilities") |
|||
const manifest = require("../manifest.json") |
|||
|
|||
const hbsInstance = handlebars.create() |
|||
registerAll(hbsInstance) |
|||
|
|||
/** |
|||
* utility function to check if the object is valid |
|||
*/ |
|||
function testObject(object) { |
|||
// JSON stringify will fail if there are any cycles, stops infinite recursion
|
|||
try { |
|||
JSON.stringify(object) |
|||
} catch (err) { |
|||
throw "Unable to process inputs to JSON, cannot recurse" |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Given an input object this will recurse through all props to try and update any handlebars statements within. |
|||
* @param {object|array} object The input structure which is to be recursed, it is important to note that |
|||
* if the structure contains any cycles then this will fail. |
|||
* @param {object} context The context that handlebars should fill data from. |
|||
* @returns {Promise<object|array>} The structure input, as fully updated as possible. |
|||
*/ |
|||
module.exports.processObject = async (object, context) => { |
|||
testObject(object) |
|||
for (let key of Object.keys(object || {})) { |
|||
if (object[key] != null) { |
|||
let val = object[key] |
|||
if (typeof val === "string") { |
|||
object[key] = await module.exports.processString(object[key], context) |
|||
} else if (typeof val === "object") { |
|||
object[key] = await module.exports.processObject(object[key], context) |
|||
} |
|||
} |
|||
} |
|||
return object |
|||
} |
|||
|
|||
/** |
|||
* This will process a single handlebars containing string. If the string passed in has no valid handlebars statements |
|||
* then nothing will occur. |
|||
* @param {string} string The template string which is the filled from the context object. |
|||
* @param {object} context An object of information which will be used to enrich the string. |
|||
* @returns {Promise<string>} The enriched string, all templates should have been replaced if they can be. |
|||
*/ |
|||
module.exports.processString = async (string, context) => { |
|||
// TODO: carry out any async calls before carrying out async call
|
|||
return module.exports.processStringSync(string, context) |
|||
} |
|||
|
|||
/** |
|||
* Given an input object this will recurse through all props to try and update any handlebars statements within. This is |
|||
* a pure sync call and therefore does not have the full functionality of the async call. |
|||
* @param {object|array} object The input structure which is to be recursed, it is important to note that |
|||
* if the structure contains any cycles then this will fail. |
|||
* @param {object} context The context that handlebars should fill data from. |
|||
* @returns {object|array} The structure input, as fully updated as possible. |
|||
*/ |
|||
module.exports.processObjectSync = (object, context) => { |
|||
testObject(object) |
|||
for (let key of Object.keys(object || {})) { |
|||
let val = object[key] |
|||
if (typeof val === "string") { |
|||
object[key] = module.exports.processStringSync(object[key], context) |
|||
} else if (typeof val === "object") { |
|||
object[key] = module.exports.processObjectSync(object[key], context) |
|||
} |
|||
} |
|||
return object |
|||
} |
|||
|
|||
/** |
|||
* This will process a single handlebars containing string. If the string passed in has no valid handlebars statements |
|||
* then nothing will occur. This is a pure sync call and therefore does not have the full functionality of the async call. |
|||
* @param {string} string The template string which is the filled from the context object. |
|||
* @param {object} context An object of information which will be used to enrich the string. |
|||
* @returns {string} The enriched string, all templates should have been replaced if they can be. |
|||
*/ |
|||
module.exports.processStringSync = (string, context) => { |
|||
if (!exports.isValid(string)) { |
|||
return string |
|||
} |
|||
// take a copy of input incase error
|
|||
const input = string |
|||
if (typeof string !== "string") { |
|||
throw "Cannot process non-string types." |
|||
} |
|||
try { |
|||
string = processors.preprocess(string) |
|||
// this does not throw an error when template can't be fulfilled, have to try correct beforehand
|
|||
const template = hbsInstance.compile(string, { |
|||
strict: false, |
|||
}) |
|||
return processors.postprocess( |
|||
template({ |
|||
now: new Date().toISOString(), |
|||
...context, |
|||
}) |
|||
) |
|||
} catch (err) { |
|||
return removeHandlebarsStatements(input) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Simple utility function which makes sure that a templating property has been wrapped in literal specifiers correctly. |
|||
* @param {string} property The property which is to be wrapped. |
|||
* @returns {string} The wrapped property ready to be added to a templating string. |
|||
*/ |
|||
module.exports.makePropSafe = property => { |
|||
return `[${property}]`.replace("[[", "[").replace("]]", "]") |
|||
} |
|||
|
|||
/** |
|||
* Checks whether or not a template string contains totally valid syntax (simply tries running it) |
|||
* @param string The string to test for valid syntax - this may contain no templates and will be considered valid. |
|||
* @returns {boolean} Whether or not the input string is valid. |
|||
*/ |
|||
module.exports.isValid = string => { |
|||
const validCases = [ |
|||
"string", |
|||
"number", |
|||
"object", |
|||
"array", |
|||
"cannot read property", |
|||
"undefined", |
|||
] |
|||
// this is a portion of a specific string always output by handlebars in the case of a syntax error
|
|||
const invalidCases = [`expecting '`] |
|||
// don't really need a real context to check if its valid
|
|||
const context = {} |
|||
try { |
|||
hbsInstance.compile(processors.preprocess(string, false))(context) |
|||
return true |
|||
} catch (err) { |
|||
const msg = err && err.message ? err.message : err |
|||
if (!msg) { |
|||
return false |
|||
} |
|||
const invalidCase = invalidCases.some(invalidCase => |
|||
msg.toLowerCase().includes(invalidCase) |
|||
) |
|||
const validCase = validCases.some(validCase => |
|||
msg.toLowerCase().includes(validCase) |
|||
) |
|||
// special case for maths functions - don't have inputs yet
|
|||
return validCase && !invalidCase |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* We have generated a static manifest file from the helpers that this string templating package makes use of. |
|||
* This manifest provides information about each of the helpers and how it can be used. |
|||
* @returns The manifest JSON which has been generated from the helpers. |
|||
*/ |
|||
module.exports.getManifest = () => { |
|||
return manifest |
|||
} |
|||
|
|||
/** |
|||
* Checks if a HBS expression is a valid JS HBS expression |
|||
* @param handlebars the HBS expression to check |
|||
* @returns {boolean} whether the expression is JS or not |
|||
*/ |
|||
module.exports.isJSBinding = handlebars => { |
|||
return module.exports.decodeJSBinding(handlebars) != null |
|||
} |
|||
|
|||
/** |
|||
* Encodes a raw JS string as a JS HBS expression |
|||
* @param javascript the JS code to encode |
|||
* @returns {string} the JS HBS expression |
|||
*/ |
|||
module.exports.encodeJSBinding = javascript => { |
|||
return `{{ js "${btoa(javascript)}" }}` |
|||
} |
|||
|
|||
/** |
|||
* Decodes a JS HBS expression to the raw JS code |
|||
* @param handlebars the JS HBS expression |
|||
* @returns {string|null} the raw JS code |
|||
*/ |
|||
module.exports.decodeJSBinding = handlebars => { |
|||
if (!handlebars || typeof handlebars !== "string") { |
|||
return null |
|||
} |
|||
|
|||
// JS is only valid if it is the only HBS expression
|
|||
if (!handlebars.trim().startsWith("{{ js ")) { |
|||
return null |
|||
} |
|||
|
|||
const captureJSRegex = new RegExp(/{{ js "(.*)" }}/) |
|||
const match = handlebars.match(captureJSRegex) |
|||
if (!match || match.length < 2) { |
|||
return null |
|||
} |
|||
return atob(match[1]) |
|||
} |
|||
Loading…
Reference in new issue