From 61d746ac8473745fae6bba82d89d81b72e09d14c Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 17 Apr 2025 09:47:07 +0100 Subject: [PATCH] Fix issue with square brackets in readable bindings --- packages/builder/src/dataBinding.js | 14 +++++++++----- packages/builder/src/dataBinding.test.js | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/builder/src/dataBinding.js b/packages/builder/src/dataBinding.js index 8ead78d8bf..f71c838f18 100644 --- a/packages/builder/src/dataBinding.js +++ b/packages/builder/src/dataBinding.js @@ -1321,11 +1321,14 @@ const shouldReplaceBinding = (currentValue, from, convertTo, binding) => { // which are substrings of other words - e.g. a binding of `a` would turn // `hah` into `h[a]h` which is obviously wrong. To avoid this we can remove all // expanded versions of the binding to be replaced. -const excludeExtensions = (string, binding) => { +const excludeReadableExtensions = (string, binding) => { + // Escape any special chars in the binding so we can treat it as a literal + // string match in the regexes below + const escaped = binding.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // Regex to find prefixed bindings (e.g. exclude xfoo for foo) - const regex1 = new RegExp(`[a-zA-Z0-9-_]+${binding}[a-zA-Z0-9-_]*`, "g") + const regex1 = new RegExp(`[a-zA-Z0-9-_]+${escaped}[a-zA-Z0-9-_]*`, "g") // Regex to find prefixed bindings (e.g. exclude foox for foo) - const regex2 = new RegExp(`[a-zA-Z0-9-_]*${binding}[a-zA-Z0-9-_]+`, "g") + const regex2 = new RegExp(`[a-zA-Z0-9-_]*${escaped}[a-zA-Z0-9-_]+`, "g") const matches = [...string.matchAll(regex1), ...string.matchAll(regex2)] for (const match of matches) { string = string.replace(match[0], new Array(match[0].length + 1).join("*")) @@ -1377,9 +1380,10 @@ const bindingReplacement = ( // in the search, working from longest to shortest so always use best match first let searchString = newBoundValue for (let from of convertFromProps) { - // Blank out all extensions of this string to avoid partial matches + // If converting readable > runtime, blank out all extensions of this + // string to avoid partial matches if (convertTo === "runtimeBinding") { - searchString = excludeExtensions(searchString, from) + searchString = excludeReadableExtensions(searchString, from) } const binding = bindableProperties.find(el => el[convertFrom] === from) if ( diff --git a/packages/builder/src/dataBinding.test.js b/packages/builder/src/dataBinding.test.js index 3b7bc94c2b..b406ef4277 100644 --- a/packages/builder/src/dataBinding.test.js +++ b/packages/builder/src/dataBinding.test.js @@ -79,6 +79,20 @@ describe("Builder dataBinding", () => { runtimeBinding: "[location]", type: "context", }, + { + category: "Bindings", + icon: "Brackets", + readableBinding: "foo.[bar]", + runtimeBinding: "[foo].[qwe]", + type: "context", + }, + { + category: "Bindings", + icon: "Brackets", + readableBinding: "foo.baz", + runtimeBinding: "[foo].[baz]", + type: "context", + }, ] it("should convert a readable binding to a runtime one", () => { const textWithBindings = `Hello {{ Current User.firstName }}! The count is {{ Binding.count }}.` @@ -102,6 +116,16 @@ describe("Builder dataBinding", () => { `location {{ _location Zlocation [location] locationZ _location_ }}` ) }) + it("should handle special characters in the readable binding", () => { + const textWithBindings = `{{ foo.baz }}` + expect( + readableToRuntimeBinding( + bindableProperties, + textWithBindings, + "runtimeBinding" + ) + ).toEqual(`{{ [foo].[baz] }}`) + }) }) describe("updateReferencesInObject", () => {