diff --git a/packages/builder/package.json b/packages/builder/package.json
index 07ba74e97f..41b98750e9 100644
--- a/packages/builder/package.json
+++ b/packages/builder/package.json
@@ -66,6 +66,7 @@
"@budibase/bbui": "^1.54.1",
"@budibase/client": "^0.5.3",
"@budibase/colorpicker": "^1.0.1",
+ "@budibase/string-templates": "^0.5.3",
"@budibase/svelte-ag-grid": "^0.0.16",
"@sentry/browser": "5.19.1",
"@svelteschool/svelte-forms": "^0.7.0",
@@ -75,7 +76,6 @@
"deepmerge": "^4.2.2",
"fast-sort": "^2.2.0",
"lodash": "^4.17.13",
- "mustache": "^4.0.1",
"posthog-js": "1.4.5",
"remixicon": "^2.5.0",
"shortid": "^2.2.15",
diff --git a/packages/builder/src/builderStore/replaceBindings.js b/packages/builder/src/builderStore/replaceBindings.js
index 0bf9f485c9..3afda0c658 100644
--- a/packages/builder/src/builderStore/replaceBindings.js
+++ b/packages/builder/src/builderStore/replaceBindings.js
@@ -1,8 +1,8 @@
-export const CAPTURE_VAR_INSIDE_MUSTACHE = /{{([^}]+)}}/g
+export const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
- // Find all instances of mustasche
- const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE)
+ // Find all instances of template strings
+ const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_TEMPLATE)
let result = textWithBindings
// Replace readableBindings with runtimeBindings
@@ -22,7 +22,7 @@ export function runtimeToReadableBinding(bindableProperties, textWithBindings) {
let temp = textWithBindings
const boundValues =
(typeof textWithBindings === "string" &&
- textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE)) ||
+ textWithBindings.match(CAPTURE_VAR_INSIDE_TEMPLATE)) ||
[]
// Replace runtimeBindings with readableBindings:
diff --git a/packages/builder/src/builderStore/uuid.js b/packages/builder/src/builderStore/uuid.js
index 5dbd9ccdbd..149da83c68 100644
--- a/packages/builder/src/builderStore/uuid.js
+++ b/packages/builder/src/builderStore/uuid.js
@@ -1,6 +1,6 @@
export function uuid() {
// always want to make this start with a letter, as this makes it
- // easier to use with mustache bindings in the client
+ // easier to use with template string bindings in the client
return "cxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0,
v = c == "x" ? r : (r & 0x3) | 0x8
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/AutomationBlockTagline.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/AutomationBlockTagline.svelte
index aca1e7f90d..2afe7dbbfa 100644
--- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/AutomationBlockTagline.svelte
+++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/AutomationBlockTagline.svelte
@@ -1,5 +1,5 @@
diff --git a/packages/builder/src/components/common/Dropdowns/DropdownItem.svelte b/packages/builder/src/components/common/Dropdowns/DropdownItem.svelte
index 66a384fc90..98fda9efb1 100644
--- a/packages/builder/src/components/common/Dropdowns/DropdownItem.svelte
+++ b/packages/builder/src/components/common/Dropdowns/DropdownItem.svelte
@@ -56,7 +56,7 @@
}
.title {
- font-weight: 400;
+ font-weight: 500;
}
.subtitle {
@@ -65,6 +65,10 @@
}
i {
- font-size: 16px;
+ padding: 0.5rem;
+ background-color: var(--grey-2);
+ font-size: 24px;
+ border-radius: var(--border-radius-s);
+ color: var(--ink);
}
diff --git a/packages/builder/src/components/userInterface/BindingPanel.svelte b/packages/builder/src/components/userInterface/BindingPanel.svelte
index 1e40e8e001..6ab2504077 100644
--- a/packages/builder/src/components/userInterface/BindingPanel.svelte
+++ b/packages/builder/src/components/userInterface/BindingPanel.svelte
@@ -1,12 +1,6 @@
-{#if constructor}
+{#if constructor && enrichedProps}
{#if children && children.length}
{#each children as child (getChildKey(child._id))}
diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js
index 357dfdaa33..54be3e51be 100644
--- a/packages/client/src/utils/buttonActions.js
+++ b/packages/client/src/utils/buttonActions.js
@@ -5,26 +5,31 @@ import { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api"
const saveRowHandler = async (action, context) => {
let draft = context[`${action.parameters.contextPath}_draft`]
if (action.parameters.fields) {
- Object.entries(action.parameters.fields).forEach(([key, entry]) => {
- draft[key] = enrichDataBinding(entry.value, context)
- })
+ for (let [key, entry] of Object.entries(action.parameters.fields)) {
+ draft[key] = await enrichDataBinding(entry.value, context)
+ }
}
await saveRow(draft)
}
const deleteRowHandler = async (action, context) => {
const { tableId, revId, rowId } = action.parameters
+ const [enrichTable, enrichRow, enrichRev] = await Promise.all([
+ enrichDataBinding(tableId, context),
+ enrichDataBinding(rowId, context),
+ enrichDataBinding(revId, context),
+ ])
await deleteRow({
- tableId: enrichDataBinding(tableId, context),
- rowId: enrichDataBinding(rowId, context),
- revId: enrichDataBinding(revId, context),
+ tableId: enrichTable,
+ rowId: enrichRow,
+ revId: enrichRev,
})
}
const triggerAutomationHandler = async (action, context) => {
const params = {}
for (let field in action.parameters.fields) {
- params[field] = enrichDataBinding(
+ params[field] = await enrichDataBinding(
action.parameters.fields[field].value,
context
)
diff --git a/packages/client/src/utils/componentProps.js b/packages/client/src/utils/componentProps.js
index be65ad2bfe..170faf47a1 100644
--- a/packages/client/src/utils/componentProps.js
+++ b/packages/client/src/utils/componentProps.js
@@ -5,7 +5,7 @@ import { enrichButtonActions } from "./buttonActions"
* Enriches component props.
* Data bindings are enriched, and button actions are enriched.
*/
-export const enrichProps = (props, dataContexts, dataBindings) => {
+export const enrichProps = async (props, dataContexts, dataBindings) => {
// Exclude all private props that start with an underscore
let validProps = {}
Object.entries(props)
@@ -24,7 +24,7 @@ export const enrichProps = (props, dataContexts, dataBindings) => {
}
// Enrich all data bindings in top level props
- let enrichedProps = enrichDataBindings(validProps, context)
+ let enrichedProps = await enrichDataBindings(validProps, context)
// Enrich button actions if they exist
if (props._component.endsWith("/button") && enrichedProps.onClick) {
diff --git a/packages/client/src/utils/enrichDataBinding.js b/packages/client/src/utils/enrichDataBinding.js
index 5de6b31a89..f6682777b5 100644
--- a/packages/client/src/utils/enrichDataBinding.js
+++ b/packages/client/src/utils/enrichDataBinding.js
@@ -1,46 +1,30 @@
-import mustache from "mustache"
+import { processString } from "@budibase/string-templates"
-// this is a much more liberal version of mustache's escape function
-// ...just ignoring < and > to prevent tags from user input
-// original version here https://github.com/janl/mustache.js/blob/4b7908f5c9fec469a11cfaed2f2bed23c84e1c5c/mustache.js#L78
-const entityMap = {
- "<": "<",
- ">": ">",
-}
-mustache.escape = text => {
- if (text == null || typeof text !== "string") {
- return text
- }
- return text.replace(/[<>]/g, function fromEntityMap(s) {
- return entityMap[s] || s
- })
-}
-
-// Regex to test inputs with to see if they are likely candidates for mustache
-const looksLikeMustache = /{{.*}}/
+// Regex to test inputs with to see if they are likely candidates for template strings
+const looksLikeTemplate = /{{.*}}/
/**
* Enriches a given input with a row from the database.
*/
-export const enrichDataBinding = (input, context) => {
+export const enrichDataBinding = async (input, context) => {
// Only accept string inputs
if (!input || typeof input !== "string") {
return input
}
- // Do a fast regex check if this looks like a mustache string
- if (!looksLikeMustache.test(input)) {
+ // Do a fast regex check if this looks like a template string
+ if (!looksLikeTemplate.test(input)) {
return input
}
- return mustache.render(input, context)
+ return processString(input, context)
}
/**
* Enriches each prop in a props object
*/
-export const enrichDataBindings = (props, context) => {
+export const enrichDataBindings = async (props, context) => {
let enrichedProps = {}
- Object.entries(props).forEach(([key, value]) => {
- enrichedProps[key] = enrichDataBinding(value, context)
- })
+ for (let [key, value] of Object.entries(props)) {
+ enrichedProps[key] = await enrichDataBinding(value, context)
+ }
return enrichedProps
}
diff --git a/packages/client/yarn.lock b/packages/client/yarn.lock
index e90defb427..9ea75db3b1 100644
--- a/packages/client/yarn.lock
+++ b/packages/client/yarn.lock
@@ -1362,11 +1362,6 @@ minimatch@^3.0.4:
dependencies:
brace-expansion "^1.1.7"
-mustache@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.0.1.tgz#d99beb031701ad433338e7ea65e0489416c854a2"
- integrity sha512-yL5VE97+OXn4+Er3THSmTdCFCtx5hHWzrolvH+JObZnUYwuaG7XV+Ch4fR2cIrcYI0tFHxS7iyFYl14bW8y2sA==
-
nwsapi@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
diff --git a/packages/server/package.json b/packages/server/package.json
index f4c6d52289..1b9973f12b 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -51,6 +51,7 @@
"dependencies": {
"@elastic/elasticsearch": "^7.10.0",
"@budibase/client": "^0.5.3",
+ "@budibase/string-templates": "^0.5.3",
"@koa/router": "^8.0.0",
"@sendgrid/mail": "^7.1.1",
"@sentry/node": "^5.19.2",
@@ -67,7 +68,6 @@
"electron-util": "^0.14.2",
"fix-path": "^3.0.0",
"fs-extra": "^8.1.0",
- "handlebars": "^4.7.6",
"jimp": "^0.16.1",
"joi": "^17.2.1",
"jsonschema": "^1.4.0",
@@ -82,7 +82,6 @@
"lodash": "^4.17.13",
"mongodb": "^3.6.3",
"mssql": "^6.2.3",
- "mustache": "^4.0.1",
"node-fetch": "^2.6.0",
"open": "^7.3.0",
"pg": "^8.5.1",
diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js
index ba381974a8..91c79f1961 100644
--- a/packages/server/src/api/controllers/application.js
+++ b/packages/server/src/api/controllers/application.js
@@ -29,7 +29,7 @@ const {
createLoginScreen,
} = require("../../constants/screens")
const { cloneDeep } = require("lodash/fp")
-const { recurseMustache } = require("../../utilities/mustache")
+const { processObject } = require("@budibase/string-templates")
const { getAllApps } = require("../../utilities")
const { USERS_TABLE_SCHEMA } = require("../../constants")
const {
@@ -232,8 +232,7 @@ const createEmptyAppPackage = async (ctx, app) => {
let screensAndLayouts = []
for (let layout of BASE_LAYOUTS) {
const cloned = cloneDeep(layout)
- cloned.title = app.name
- screensAndLayouts.push(recurseMustache(cloned, app))
+ screensAndLayouts.push(await processObject(cloned, app))
}
const homeScreen = createHomeScreen(app)
diff --git a/packages/server/src/api/controllers/static/index.js b/packages/server/src/api/controllers/static/index.js
index 2ca9d5ce60..2096b3039e 100644
--- a/packages/server/src/api/controllers/static/index.js
+++ b/packages/server/src/api/controllers/static/index.js
@@ -7,7 +7,7 @@ const fs = require("fs-extra")
const uuid = require("uuid")
const AWS = require("aws-sdk")
const { prepareUpload } = require("../deploy/utils")
-const handlebars = require("handlebars")
+const { processString } = require("@budibase/string-templates")
const {
budibaseAppsDir,
budibaseTempDir,
@@ -176,11 +176,8 @@ exports.serveApp = async function(ctx) {
objectStoreUrl: objectStoreUrl(),
})
- const template = handlebars.compile(
- fs.readFileSync(`${__dirname}/templates/app.hbs`, "utf8")
- )
-
- ctx.body = template({
+ const appHbs = fs.readFileSync(`${__dirname}/templates/app.hbs`, "utf8")
+ ctx.body = await processString(appHbs, {
head,
body: html,
style: css.code,
diff --git a/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte b/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte
index 71cb81b9cf..a787521394 100644
--- a/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte
+++ b/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte
@@ -22,7 +22,9 @@
{title}
-
+
diff --git a/packages/string-templates/.eslintrc b/packages/string-templates/.eslintrc
new file mode 100644
index 0000000000..3431bf04fb
--- /dev/null
+++ b/packages/string-templates/.eslintrc
@@ -0,0 +1,12 @@
+{
+ "globals": {
+ "emit": true,
+ "key": true
+ },
+ "env": {
+ "node": true
+ },
+ "extends": ["eslint:recommended"],
+ "rules": {
+ }
+}
\ No newline at end of file
diff --git a/packages/string-templates/.gitignore b/packages/string-templates/.gitignore
new file mode 100644
index 0000000000..1eae0cf670
--- /dev/null
+++ b/packages/string-templates/.gitignore
@@ -0,0 +1,2 @@
+dist/
+node_modules/
diff --git a/packages/string-templates/jest.config.js b/packages/string-templates/jest.config.js
new file mode 100644
index 0000000000..c6391cdb92
--- /dev/null
+++ b/packages/string-templates/jest.config.js
@@ -0,0 +1,194 @@
+/*
+ * For a detailed explanation regarding each configuration property, visit:
+ * https://jestjs.io/docs/en/configuration.html
+ */
+
+module.exports = {
+ // All imported modules in your tests should be mocked automatically
+ // automock: false,
+
+ // Stop running tests after `n` failures
+ // bail: 0,
+
+ // The directory where Jest should store its cached dependency information
+ // cacheDirectory: "/tmp/jest_rs",
+
+ // Automatically clear mock calls and instances between every test
+ clearMocks: true,
+
+ // Indicates whether the coverage information should be collected while executing the test
+ // collectCoverage: false,
+
+ // An array of glob patterns indicating a set of files for which coverage information should be collected
+ // collectCoverageFrom: undefined,
+
+ // The directory where Jest should output its coverage files
+ coverageDirectory: "coverage",
+
+ // An array of regexp pattern strings used to skip coverage collection
+ // coveragePathIgnorePatterns: [
+ // "/node_modules/"
+ // ],
+
+ // Indicates which provider should be used to instrument code for coverage
+ coverageProvider: "v8",
+
+ // A list of reporter names that Jest uses when writing coverage reports
+ // coverageReporters: [
+ // "json",
+ // "text",
+ // "lcov",
+ // "clover"
+ // ],
+
+ // An object that configures minimum threshold enforcement for coverage results
+ // coverageThreshold: undefined,
+
+ // A path to a custom dependency extractor
+ // dependencyExtractor: undefined,
+
+ // Make calling deprecated APIs throw helpful error messages
+ // errorOnDeprecated: false,
+
+ // Force coverage collection from ignored files using an array of glob patterns
+ // forceCoverageMatch: [],
+
+ // A path to a module which exports an async function that is triggered once before all test suites
+ // globalSetup: undefined,
+
+ // A path to a module which exports an async function that is triggered once after all test suites
+ // globalTeardown: undefined,
+
+ // A set of global variables that need to be available in all test environments
+ // globals: {},
+
+ // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
+ // maxWorkers: "50%",
+
+ // An array of directory names to be searched recursively up from the requiring module's location
+ // moduleDirectories: [
+ // "node_modules"
+ // ],
+
+ // An array of file extensions your modules use
+ // moduleFileExtensions: [
+ // "js",
+ // "json",
+ // "jsx",
+ // "ts",
+ // "tsx",
+ // "node"
+ // ],
+
+ // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
+ // moduleNameMapper: {},
+
+ // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
+ // modulePathIgnorePatterns: [],
+
+ // Activates notifications for test results
+ // notify: false,
+
+ // An enum that specifies notification mode. Requires { notify: true }
+ // notifyMode: "failure-change",
+
+ // A preset that is used as a base for Jest's configuration
+ // preset: undefined,
+
+ // Run tests from one or more projects
+ // projects: undefined,
+
+ // Use this configuration option to add custom reporters to Jest
+ // reporters: undefined,
+
+ // Automatically reset mock state between every test
+ // resetMocks: false,
+
+ // Reset the module registry before running each individual test
+ // resetModules: false,
+
+ // A path to a custom resolver
+ // resolver: undefined,
+
+ // Automatically restore mock state between every test
+ // restoreMocks: false,
+
+ // The root directory that Jest should scan for tests and modules within
+ // rootDir: undefined,
+
+ // A list of paths to directories that Jest should use to search for files in
+ // roots: [
+ // ""
+ // ],
+
+ // Allows you to use a custom runner instead of Jest's default test runner
+ // runner: "jest-runner",
+
+ // The paths to modules that run some code to configure or set up the testing environment before each test
+ // setupFiles: [],
+
+ // A list of paths to modules that run some code to configure or set up the testing framework before each test
+ // setupFilesAfterEnv: [],
+
+ // The number of seconds after which a test is considered as slow and reported as such in the results.
+ // slowTestThreshold: 5,
+
+ // A list of paths to snapshot serializer modules Jest should use for snapshot testing
+ // snapshotSerializers: [],
+
+ // The test environment that will be used for testing
+ testEnvironment: "node",
+
+ // Options that will be passed to the testEnvironment
+ // testEnvironmentOptions: {},
+
+ // Adds a location field to test results
+ // testLocationInResults: false,
+
+ // The glob patterns Jest uses to detect test files
+ // testMatch: [
+ // "**/__tests__/**/*.[jt]s?(x)",
+ // "**/?(*.)+(spec|test).[tj]s?(x)"
+ // ],
+
+ // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
+ // testPathIgnorePatterns: [
+ // "/node_modules/"
+ // ],
+
+ // The regexp pattern or array of patterns that Jest uses to detect test files
+ // testRegex: [],
+
+ // This option allows the use of a custom results processor
+ // testResultsProcessor: undefined,
+
+ // This option allows use of a custom test runner
+ // testRunner: "jasmine2",
+
+ // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
+ // testURL: "http://localhost",
+
+ // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
+ // timers: "real",
+
+ // A map from regular expressions to paths to transformers
+ // transform: undefined,
+
+ // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
+ // transformIgnorePatterns: [
+ // "/node_modules/",
+ // "\\.pnp\\.[^\\/]+$"
+ // ],
+
+ // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
+ // unmockedModulePathPatterns: undefined,
+
+ // Indicates whether each individual test should be reported during the run
+ // verbose: undefined,
+
+ // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
+ // watchPathIgnorePatterns: [],
+
+ // Whether to use watchman for file crawling
+ // watchman: true,
+}
diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json
new file mode 100644
index 0000000000..77e8f7ded5
--- /dev/null
+++ b/packages/string-templates/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@budibase/string-templates",
+ "version": "0.5.3",
+ "description": "Handlebars wrapper for Budibase templating.",
+ "main": "dist/bundle.js",
+ "module": "dist/bundle.js",
+ "license": "AGPL-3.0",
+ "types": "dist/index.d.ts",
+ "scripts": {
+ "build": "tsc && rollup -c",
+ "dev:builder": "tsc && rollup -cw",
+ "test": "jest"
+ },
+ "dependencies": {
+ "handlebars": "^4.7.6",
+ "handlebars-helpers": "^0.10.0",
+ "handlebars-utils": "^1.0.6",
+ "helper-date": "^1.0.1",
+ "lodash": "^4.17.20"
+ },
+ "devDependencies": {
+ "@rollup/plugin-json": "^4.1.0",
+ "jest": "^26.6.3",
+ "rollup": "^2.36.2",
+ "rollup-plugin-commonjs": "^10.1.0",
+ "rollup-plugin-node-builtins": "^2.1.2",
+ "rollup-plugin-node-globals": "^1.4.0",
+ "rollup-plugin-node-resolve": "^5.2.0",
+ "typescript": "^4.1.3"
+ }
+}
diff --git a/packages/string-templates/rollup.config.js b/packages/string-templates/rollup.config.js
new file mode 100644
index 0000000000..5a71316fb5
--- /dev/null
+++ b/packages/string-templates/rollup.config.js
@@ -0,0 +1,28 @@
+import commonjs from "rollup-plugin-commonjs"
+import resolve from "rollup-plugin-node-resolve"
+import builtins from "rollup-plugin-node-builtins"
+import globals from "rollup-plugin-node-globals"
+import json from "@rollup/plugin-json"
+
+export default {
+ input: "src/index.js",
+ output: [
+ {
+ sourcemap: true,
+ format: "umd",
+ file: "./dist/bundle.js",
+ name: "string-templates",
+ exports: "named",
+ },
+ ],
+ plugins: [
+ resolve({
+ preferBuiltins: true,
+ browser: true,
+ }),
+ commonjs(),
+ globals(),
+ builtins(),
+ json(),
+ ],
+}
diff --git a/packages/string-templates/src/helpers/Helper.js b/packages/string-templates/src/helpers/Helper.js
new file mode 100644
index 0000000000..8eee332678
--- /dev/null
+++ b/packages/string-templates/src/helpers/Helper.js
@@ -0,0 +1,19 @@
+class Helper {
+ constructor(name, fn) {
+ this.name = name
+ this.fn = fn
+ }
+
+ register(handlebars) {
+ // wrap the function so that no helper can cause handlebars to break
+ handlebars.registerHelper(this.name, value => {
+ return this.fn(value) || value
+ })
+ }
+
+ unregister(handlebars) {
+ handlebars.unregisterHelper(this.name)
+ }
+}
+
+module.exports = Helper
diff --git a/packages/string-templates/src/helpers/constants.js b/packages/string-templates/src/helpers/constants.js
new file mode 100644
index 0000000000..124ba6f277
--- /dev/null
+++ b/packages/string-templates/src/helpers/constants.js
@@ -0,0 +1,21 @@
+module.exports.HelperFunctionBuiltin = [
+ "#if",
+ "#unless",
+ "#each",
+ "#with",
+ "lookup",
+ "log",
+ "blockHelperMissing",
+ "each",
+ "helperMissing",
+ "if",
+ "unless",
+ "log",
+ "lookup",
+ "with",
+]
+
+module.exports.HelperFunctionNames = {
+ OBJECT: "object",
+ ALL: "all",
+}
diff --git a/packages/string-templates/src/helpers/external.js b/packages/string-templates/src/helpers/external.js
new file mode 100644
index 0000000000..7c767db5f4
--- /dev/null
+++ b/packages/string-templates/src/helpers/external.js
@@ -0,0 +1,56 @@
+const helpers = require("handlebars-helpers")
+const dateHelper = require("helper-date")
+const { HelperFunctionBuiltin } = require("./constants")
+
+/**
+ * full list of supported helpers can be found here:
+ * https://github.com/helpers/handlebars-helpers
+ */
+
+const EXTERNAL_FUNCTION_COLLECTIONS = [
+ "math",
+ "array",
+ "number",
+ "url",
+ "string",
+ "markdown",
+]
+
+const DATE_NAME = "date"
+
+exports.registerAll = handlebars => {
+ handlebars.registerHelper(DATE_NAME, dateHelper)
+ let externalNames = []
+ for (let collection of EXTERNAL_FUNCTION_COLLECTIONS) {
+ // collect information about helper
+ let hbsHelperInfo = helpers[collection]()
+ for (let entry of Object.entries(hbsHelperInfo)) {
+ const name = entry[0]
+ // skip built in functions and ones seen already
+ if (
+ HelperFunctionBuiltin.indexOf(name) !== -1 ||
+ externalNames.indexOf(name) !== -1
+ ) {
+ continue
+ }
+ externalNames.push(name)
+ }
+ // attach it to our handlebars instance
+ helpers[collection]({
+ handlebars,
+ })
+ }
+ // add date external functionality
+ externalNames.push(DATE_NAME)
+ exports.externalHelperNames = externalNames
+}
+
+exports.unregisterAll = handlebars => {
+ handlebars.unregisterHelper(DATE_NAME)
+ for (let name of module.exports.externalHelperNames) {
+ handlebars.unregisterHelper(name)
+ }
+ exports.externalHelperNames = []
+}
+
+exports.externalHelperNames = []
diff --git a/packages/string-templates/src/helpers/index.js b/packages/string-templates/src/helpers/index.js
new file mode 100644
index 0000000000..e5cf3c7a01
--- /dev/null
+++ b/packages/string-templates/src/helpers/index.js
@@ -0,0 +1,53 @@
+const Helper = require("./Helper")
+const { SafeString } = require("handlebars")
+const externalHandlebars = require("./external")
+const { HelperFunctionNames, HelperFunctionBuiltin } = require("./constants")
+
+const HTML_SWAPS = {
+ "<": "<",
+ ">": ">",
+}
+
+const HELPERS = [
+ // external helpers
+ new Helper(HelperFunctionNames.OBJECT, value => {
+ return new SafeString(JSON.stringify(value))
+ }),
+ // this help is applied to all statements
+ new Helper(HelperFunctionNames.ALL, value => {
+ // null/undefined values produce bad results
+ if (value == null) {
+ return ""
+ }
+ let text = new SafeString(unescape(value).replace(/&/g, "&"))
+ if (text == null || typeof text !== "string") {
+ return text
+ }
+ return text.replace(/[<>]/g, tag => {
+ return HTML_SWAPS[tag] || tag
+ })
+ }),
+]
+
+module.exports.HelperNames = () => {
+ return Object.values(HelperFunctionNames).concat(
+ HelperFunctionBuiltin,
+ externalHandlebars.externalHelperNames
+ )
+}
+
+module.exports.registerAll = handlebars => {
+ for (let helper of HELPERS) {
+ helper.register(handlebars)
+ }
+ // register imported helpers
+ externalHandlebars.registerAll(handlebars)
+}
+
+module.exports.unregisterAll = handlebars => {
+ for (let helper of HELPERS) {
+ helper.unregister(handlebars)
+ }
+ // unregister all imported helpers
+ externalHandlebars.unregisterAll(handlebars)
+}
diff --git a/packages/string-templates/src/index.js b/packages/string-templates/src/index.js
new file mode 100644
index 0000000000..2636752488
--- /dev/null
+++ b/packages/string-templates/src/index.js
@@ -0,0 +1,105 @@
+const handlebars = require("handlebars")
+const { registerAll } = require("./helpers/index")
+const processors = require("./processors")
+const { cloneDeep } = require("lodash/fp")
+const { removeNull } = require("./utilities")
+
+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