From a9274f7d865dc66f19d9fabe732cd4e307de10a8 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 20 Jan 2021 18:12:16 +0000 Subject: [PATCH] Adding cleaners which will process and find spaces removing them and fixing them with literal specifiers for handlebars props. Also changing the way cleaners work for the system to make it easier to add them. --- packages/string-templates/src/cleaning.js | 91 +++++++++++++++++++ .../string-templates/src/helpers/index.js | 22 ++++- packages/string-templates/src/index.js | 47 +--------- .../string-templates/test/escapes.spec.js | 14 +++ 4 files changed, 125 insertions(+), 49 deletions(-) create mode 100644 packages/string-templates/src/cleaning.js 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",