From 4fd31b9eacef8e996ba9e6d83d7cda8baed408ce Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 11 Oct 2021 14:53:55 +0100 Subject: [PATCH] Add JS helper to string templates --- .../string-templates/src/helpers/Helper.js | 5 +- .../string-templates/src/helpers/constants.js | 1 + .../string-templates/src/helpers/index.js | 3 + .../src/helpers/javascript.js | 66 +++++++++++++++++++ packages/string-templates/src/index.cjs | 8 +++ packages/string-templates/src/index.mjs | 3 + 6 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 packages/string-templates/src/helpers/javascript.js diff --git a/packages/string-templates/src/helpers/Helper.js b/packages/string-templates/src/helpers/Helper.js index 8eee332678..3d7a9316f3 100644 --- a/packages/string-templates/src/helpers/Helper.js +++ b/packages/string-templates/src/helpers/Helper.js @@ -6,8 +6,9 @@ class Helper { register(handlebars) { // wrap the function so that no helper can cause handlebars to break - handlebars.registerHelper(this.name, value => { - return this.fn(value) || value + handlebars.registerHelper(this.name, (value, info) => { + const context = info?.data?.root + return this.fn(value, context || {}) || value }) } diff --git a/packages/string-templates/src/helpers/constants.js b/packages/string-templates/src/helpers/constants.js index f0d004060a..1f8cf7a59d 100644 --- a/packages/string-templates/src/helpers/constants.js +++ b/packages/string-templates/src/helpers/constants.js @@ -19,6 +19,7 @@ module.exports.HelperFunctionNames = { OBJECT: "object", ALL: "all", LITERAL: "literal", + JS: "js", } module.exports.LITERAL_MARKER = "%LITERAL%" diff --git a/packages/string-templates/src/helpers/index.js b/packages/string-templates/src/helpers/index.js index 23a20c90c7..a8c5ee0752 100644 --- a/packages/string-templates/src/helpers/index.js +++ b/packages/string-templates/src/helpers/index.js @@ -1,6 +1,7 @@ const Helper = require("./Helper") const { SafeString } = require("handlebars") const externalHandlebars = require("./external") +const { processJS } = require("./javascript") const { HelperFunctionNames, HelperFunctionBuiltin, @@ -17,6 +18,8 @@ const HELPERS = [ new Helper(HelperFunctionNames.OBJECT, value => { return new SafeString(JSON.stringify(value)) }), + // javascript helper + new Helper(HelperFunctionNames.JS, processJS), // this help is applied to all statements new Helper(HelperFunctionNames.ALL, value => { if ( diff --git a/packages/string-templates/src/helpers/javascript.js b/packages/string-templates/src/helpers/javascript.js new file mode 100644 index 0000000000..49e3561d8d --- /dev/null +++ b/packages/string-templates/src/helpers/javascript.js @@ -0,0 +1,66 @@ +const CAPTURE_JS = new RegExp(/{{ js "(.*)" }}/) +const vm = require("vm") + +// Helper utility to strip square brackets from a value +const removeSquareBrackets = value => { + if (!value || typeof value !== "string") { + return value + } + const regex = /\[+(.+)]+/ + const matches = value.match(regex) + if (matches && matches[1]) { + return matches[1] + } + return value +} + +// Our context getter function provided to JS code as $. +// Extracts a value from context. +const getContextValue = (path, context) => { + let data = context + path.split(".").forEach(key => { + data = data[removeSquareBrackets(key)] || {} + }) + return data +} + +// Evaluates JS code against a certain context +module.exports.processJS = (handlebars, context) => { + try { + // Wrap JS in a function and immediately invoke it. + // This is required to allow the final `return` statement to be valid. + const js = `function run(){${atob(handlebars)}};run();` + + // Our $ context function gets a value from context + const sandboxContext = { $: path => getContextValue(path, context) } + + // Create a sandbox with out context and run the JS + vm.createContext(sandboxContext) + return vm.runInNewContext(js, sandboxContext) + } catch (error) { + console.warn(error) + return "Error while executing JS" + } +} + +// Checks if a HBS expression is a valid JS HBS expression +module.exports.isJSBinding = handlebars => { + return module.exports.decodeJSBinding(handlebars) != null +} + +// Encodes a raw JS string as a JS HBS expression +module.exports.encodeJSBinding = javascript => { + return `{{ js "${btoa(javascript)}" }}` +} + +// Decodes a JS HBS expression to the raw JS string +module.exports.decodeJSBinding = handlebars => { + if (!handlebars || typeof handlebars !== "string") { + return null + } + const match = handlebars.match(CAPTURE_JS) + if (!match || match.length < 2) { + return null + } + return atob(match[1]) +} diff --git a/packages/string-templates/src/index.cjs b/packages/string-templates/src/index.cjs index 05f8c69f5a..31de252381 100644 --- a/packages/string-templates/src/index.cjs +++ b/packages/string-templates/src/index.cjs @@ -3,6 +3,7 @@ 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) @@ -159,3 +160,10 @@ module.exports.isValid = string => { 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 \ No newline at end of file diff --git a/packages/string-templates/src/index.mjs b/packages/string-templates/src/index.mjs index 90e62b5888..855bd97a55 100644 --- a/packages/string-templates/src/index.mjs +++ b/packages/string-templates/src/index.mjs @@ -6,6 +6,9 @@ import templates from "./index.cjs" export const isValid = templates.isValid export const makePropSafe = templates.makePropSafe export const getManifest = templates.getManifest +export const isJSBinding = templates.isJSBinding +export const encodeJSBinding = templates.encodeJSBinding +export const decodeJSBinding = templates.decodeJSBinding export const processStringSync = templates.processStringSync export const processObjectSync = templates.processObjectSync export const processString = templates.processString