Add JS helper to string templates

This commit is contained in:
Andrew Kingston 2021-10-11 14:53:55 +01:00
parent 98ca5da073
commit 4fd31b9eac
6 changed files with 84 additions and 2 deletions

View File

@ -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
})
}

View File

@ -19,6 +19,7 @@ module.exports.HelperFunctionNames = {
OBJECT: "object",
ALL: "all",
LITERAL: "literal",
JS: "js",
}
module.exports.LITERAL_MARKER = "%LITERAL%"

View File

@ -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 (

View File

@ -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])
}

View File

@ -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

View File

@ -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