Skip to content

Commit 63302e9

Browse files
authored
fix: improve regex of replacer in evaluateDeep (#968)
replace regex replacer with stack implementation for evaluateDeep
1 parent fee049c commit 63302e9

File tree

4 files changed

+167
-7
lines changed

4 files changed

+167
-7
lines changed

.changeset/good-chicken-wonder.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ensembleui/react-framework": patch
3+
---
4+
5+
fixed the regression related invokeAPI inputs evaluation

packages/framework/src/shared/__tests__/common.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { evaluateDeep } from "../../evaluate";
12
import { findExpressions, isCompoundExpression } from "../common";
23

34
test("find deeply nested expressions", () => {
@@ -82,4 +83,115 @@ describe("isCompoundExpression", () => {
8283
).toBe(false);
8384
});
8485
});
86+
87+
describe("validate expressions", () => {
88+
test("Single placeholder with no additional text", () => {
89+
expect(
90+
evaluateDeep({ name: "${name}" }, undefined, {
91+
name: "Ensemble",
92+
}),
93+
).toMatchObject({ name: "Ensemble" });
94+
});
95+
96+
test("Single placeholder with additional text", () => {
97+
expect(
98+
evaluateDeep({ name: "${name} framework" }, undefined, {
99+
name: "Ensemble",
100+
}),
101+
).toMatchObject({ name: "Ensemble framework" });
102+
});
103+
104+
test("Multiple placeholders", () => {
105+
expect(
106+
evaluateDeep({ name: "${name} ${platform}" }, undefined, {
107+
name: "Ensemble",
108+
platform: "Web",
109+
}),
110+
).toMatchObject({ name: "Ensemble Web" });
111+
});
112+
113+
test("Single complex placeholder with expression", () => {
114+
expect(
115+
evaluateDeep({ name: "${name + platform}" }, undefined, {
116+
name: "Ensemble",
117+
platform: "Web",
118+
}),
119+
).toMatchObject({ name: "EnsembleWeb" });
120+
});
121+
122+
test("Nested template literals (complex case)", () => {
123+
expect(
124+
evaluateDeep(
125+
{
126+
name: "${`Ensemble ${platform} platform`}",
127+
},
128+
undefined,
129+
{
130+
name: "Ensemble",
131+
platform: "Web",
132+
},
133+
),
134+
).toMatchObject({ name: "Ensemble Web platform" });
135+
});
136+
137+
test("No placeholders", () => {
138+
expect(
139+
evaluateDeep(
140+
{
141+
name: "Ensemble Web",
142+
},
143+
undefined,
144+
{
145+
name: "Ensemble",
146+
platform: "Web",
147+
},
148+
),
149+
).toMatchObject({ name: "Ensemble Web" });
150+
});
151+
152+
test("Placeholder at the start and end", () => {
153+
expect(
154+
evaluateDeep(
155+
{
156+
name: "${name} Web ${platform}",
157+
},
158+
undefined,
159+
{
160+
name: "Ensemble",
161+
platform: "Studio",
162+
},
163+
),
164+
).toMatchObject({ name: "Ensemble Web Studio" });
165+
});
166+
167+
test("Placeholder within text", () => {
168+
expect(
169+
evaluateDeep(
170+
{
171+
name: "Ensemble ${platform} Studio",
172+
},
173+
undefined,
174+
{
175+
name: "Ensemble",
176+
platform: "Web",
177+
},
178+
),
179+
).toMatchObject({ name: "Ensemble Web Studio" });
180+
});
181+
182+
test("Map loop", () => {
183+
expect(
184+
evaluateDeep(
185+
{
186+
name: "${['X', 'Y'].map((c) => { return c.toLowerCase() })}",
187+
},
188+
undefined,
189+
{
190+
name: "Ensemble",
191+
platform: "Web",
192+
},
193+
),
194+
).toMatchObject({ name: ["x", "y"] });
195+
});
196+
});
85197
/* eslint-enable no-template-curly-in-string */

packages/framework/src/shared/common.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,16 +160,59 @@ export const error = (value: unknown): void => {
160160
console.error(value);
161161
};
162162

163+
export const validateExpressions = (
164+
value: string,
165+
): { isValid: boolean; expressions: string[] } => {
166+
const expressions: string[] = [];
167+
let currentExpr = "";
168+
let nestCount = 0;
169+
170+
for (let i = 0; i < value.length; i++) {
171+
if (value[i] === "$" && value[i + 1] === "{") {
172+
if (nestCount === 0) {
173+
currentExpr = "";
174+
}
175+
nestCount++;
176+
currentExpr += value.slice(i, i + 2);
177+
i++;
178+
continue;
179+
}
180+
181+
if (nestCount > 0) {
182+
currentExpr += value[i];
183+
if (value[i] === "{") {
184+
nestCount++;
185+
} else if (value[i] === "}") {
186+
nestCount--;
187+
if (nestCount === 0) {
188+
expressions.push(currentExpr);
189+
}
190+
}
191+
}
192+
}
193+
194+
return {
195+
isValid: nestCount === 0 && expressions.length > 0,
196+
expressions,
197+
};
198+
};
199+
163200
export const replace =
164201
(replacer: (expr: string) => string) =>
165202
(val: string): unknown => {
166-
const matches = val.match(/\$\{+[^}]+\}+/g);
167-
if (matches?.length === 1 && matches[0] === val) {
168-
return replacer(val);
203+
const { expressions, isValid } = validateExpressions(val);
204+
if (isValid) {
205+
if (expressions.length === 1 && val === expressions[0]) {
206+
return replacer(expressions[0]);
207+
}
208+
209+
let replacedValue = val;
210+
expressions.forEach((expr) => {
211+
replacedValue = replacedValue.replace(expr, replacer(expr));
212+
});
213+
return replacedValue;
169214
}
170-
return val.replace(/\$\{+[^}]+\}+/g, (expression) => {
171-
return replacer(expression);
172-
});
215+
return val;
173216
};
174217

175218
export const visitExpressions = (

packages/runtime/src/runtime/hooks/useEnsembleAction.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,8 @@ export const useInvokeAPI: EnsembleActionHook<InvokeAPIAction> = (action) => {
218218
DataFetcher.fetch(
219219
currentApi,
220220
{
221-
...evaluatedInputs,
222221
...context,
222+
...evaluatedInputs,
223223
ensemble: {
224224
env: appContext?.env,
225225
secrets: appContext?.secrets,

0 commit comments

Comments
 (0)