diff --git a/packages/string-templates/src/cleaning.js b/packages/string-templates/src/cleaning.js new file mode 100644 index 0000000000..671382da10 --- /dev/null +++ b/packages/string-templates/src/cleaning.js @@ -0,0 +1,91 @@ +const { HelperFunctions } = require("./helpers/index") + +const HBS_CLEANING_REGEX = /{{[^}}]*}}/g +const ALPHA_NUMERIC_REGEX = /^[A-Za-z0-9]+$/g + +function isAlphaNumeric(char) { + return char.match(ALPHA_NUMERIC_REGEX) +} + +function swapStrings(string, start, length, swap) { + return string.slice(0, start) + swap + string.slice(start + length) +} + +function handleCleaner(string, match, fn) { + const output = fn(match) + const idx = string.indexOf(match) + return swapStrings(string, idx, match.length, output) +} + +function swapToDotNotation(statement) { + let startBraceIdx = statement.indexOf("[") + let lastIdx = 0 + while (startBraceIdx !== -1) { + // if the character previous to the literal specifier is alpha-numeric this should happen + if (isAlphaNumeric(statement.charAt(startBraceIdx - 1))) { + statement = swapStrings(statement, startBraceIdx + lastIdx, 1, ".[") + } + lastIdx = startBraceIdx + 1 + startBraceIdx = statement.substring(lastIdx + 1).indexOf("[") + } + return statement +} + +function handleSpacesInProperties(statement) { + // exclude helpers and brackets, regex will only find double brackets + const exclusions = HelperFunctions.concat(["{{", "}}"]) + // find all the parts split by spaces + const splitBySpaces = statement.split(" ") + // remove the excluded elements + const propertyParts = splitBySpaces.filter(part => exclusions.indexOf(part) === -1) + // rebuild to get the full property + const fullProperty = propertyParts.join(" ") + // now work out the dot notation layers and split them up + const propertyLayers = fullProperty.split(".") + // find the layers which need to be wrapped and wrap them + for (let layer of propertyLayers) { + if (layer.indexOf(" ") !== -1) { + statement = swapStrings(statement, statement.indexOf(layer), layer.length, `[${layer}]`) + } + } + // remove the edge case of double brackets being entered (in-case user already has specified) + return statement.replace(/\[\[/g, "[").replace(/]]/g, "]") +} + +function finalise(statement) { + let insideStatement = statement.slice(2, statement.length - 2) + if (insideStatement.charAt(0) === " ") { + insideStatement = insideStatement.slice(1) + } + if (insideStatement.charAt(insideStatement.length - 1) === " ") { + insideStatement = insideStatement.slice(0, insideStatement.length - 1) + } + return `{{ all (${insideStatement}) }}` +} + +/** + * When running handlebars statements to execute on the context of the automation it possible user's may input handlebars + * in a few different forms, some of which are invalid but are logically valid. An example of this would be the handlebars + * statement "{{steps[0].revision}}" here it is obvious the user is attempting to access an array or object using array + * like operators. These are not supported by handlebars and therefore the statement will fail. This function will clean up + * the handlebars statement so it instead reads as "{{steps.0.revision}}" which is valid and will work. It may also be expanded + * to include any other handlebars statement cleanup that has been deemed necessary for the system. + * + * @param {string} string The string which *may* contain handlebars statements, it is OK if it does not contain any. + * @returns {string} The string that was input with cleaned up handlebars statements as required. + */ +module.exports.cleanHandlebars = (string) => { + let cleaners = [swapToDotNotation, handleSpacesInProperties, finalise] + for (let cleaner of cleaners) { + // re-run search each time incase previous cleaner update/removed a match + let regex = new RegExp(HBS_CLEANING_REGEX) + let matches = string.match(regex) + if (matches == null) { + continue + } + for (let match of matches) { + string = handleCleaner(string, match, cleaner) + } + } + return string +} \ No newline at end of file diff --git a/packages/string-templates/src/helpers/index.js b/packages/string-templates/src/helpers/index.js index cbef0fb473..498ac40dce 100644 --- a/packages/string-templates/src/helpers/index.js +++ b/packages/string-templates/src/helpers/index.js @@ -6,14 +6,28 @@ const HTML_SWAPS = { ">": ">", } +const HelperFunctionBuiltin = [ + "#if", + "#unless", + "#each", + "#with", + "lookup", + "log" +] + +const HelperFunctionNames = { + OBJECT: "object", + ALL: "all", +} + const HELPERS = [ // external helpers - new Helper("object", value => { + new Helper(HelperFunctionNames.OBJECT, value => { return new SafeString(JSON.stringify(value)) }), // this help is applied to all statements - new Helper("all", value => { - let text = unescape(value).replace(/&/g, '&'); + new Helper(HelperFunctionNames.ALL, value => { + let text = new SafeString(unescape(value).replace(/&/g, '&')) if (text == null || typeof text !== "string") { return text } @@ -23,6 +37,8 @@ const HELPERS = [ }) ] +module.exports.HelperFunctions = Object.values(HelperFunctionNames).concat(HelperFunctionBuiltin) + module.exports.registerAll = handlebars => { for (let helper of HELPERS) { helper.register(handlebars) diff --git a/packages/string-templates/src/index.js b/packages/string-templates/src/index.js index 033fe5c1a8..a6b95a600d 100644 --- a/packages/string-templates/src/index.js +++ b/packages/string-templates/src/index.js @@ -1,54 +1,10 @@ const handlebars = require("handlebars") const { registerAll } = require("./helpers/index") - -const HBS_CLEANING_REGEX = /{{[^}}]*}}/g -const FIND_HBS_REGEX = /{{.*}}/ +const { cleanHandlebars } = require("./cleaning") const hbsInstance = handlebars.create() registerAll(hbsInstance) -/** - * When running handlebars statements to execute on the context of the automation it possible user's may input handlebars - * in a few different forms, some of which are invalid but are logically valid. An example of this would be the handlebars - * statement "{{steps[0].revision}}" here it is obvious the user is attempting to access an array or object using array - * like operators. These are not supported by handlebars and therefore the statement will fail. This function will clean up - * the handlebars statement so it instead reads as "{{steps.0.revision}}" which is valid and will work. It may also be expanded - * to include any other handlebars statement cleanup that has been deemed necessary for the system. - * - * @param {string} string The string which *may* contain handlebars statements, it is OK if it does not contain any. - * @returns {string} The string that was input with cleaned up handlebars statements as required. - */ -function cleanHandlebars(string) { - // TODO: handle these types of statement - // every statement must have the "all" helper added e.g. - // {{ person }} => {{ html person }} - // escaping strings must be handled as such: - // {{ person name }} => {{ [person name] }} - // {{ obj.person name }} => {{ obj.[person name] }} - let charToReplace = { - "[": ".", - "]": "", - } - let regex = new RegExp(HBS_CLEANING_REGEX) - let matches = string.match(regex) - if (matches == null) { - return string - } - for (let match of matches) { - let baseIdx = string.indexOf(match) - for (let key of Object.keys(charToReplace)) { - let idxChar = match.indexOf(key) - if (idxChar !== -1) { - string = - string.slice(baseIdx, baseIdx + idxChar) + - charToReplace[key] + - string.slice(baseIdx + idxChar + 1) - } - } - } - return string -} - /** * utility function to check if the object is valid */ @@ -70,7 +26,6 @@ function testObject(object) { */ module.exports.processObject = async (object, context) => { testObject(object) - // TODO: carry out any async calls before carrying out async call for (let key of Object.keys(object)) { let val = object[key] if (typeof val === "string") { diff --git a/packages/string-templates/test/escapes.spec.js b/packages/string-templates/test/escapes.spec.js index de73e83e67..eb94b1ce2e 100644 --- a/packages/string-templates/test/escapes.spec.js +++ b/packages/string-templates/test/escapes.spec.js @@ -3,6 +3,20 @@ const { } = require("../src/index") describe("Handling context properties with spaces in their name", () => { + it("should allow through literal specifiers", async () => { + const output = await processString("test {{ [test thing] }}", { + "test thing": 1 + }) + expect(output).toBe("test 1") + }) + + it("should convert to dot notation where required", async () => { + const output = await processString("test {{ test[0] }}", { + test: [2] + }) + expect(output).toBe("test 2") + }) + it("should be able to handle a property with a space in its name", async () => { const output = await processString("hello my name is {{ person name }}", { "person name": "Mike",