From b855d8c69595cdf67b67d503b262836131e8bf33 Mon Sep 17 00:00:00 2001 From: Fabien Arcellier Date: Mon, 23 Sep 2024 09:55:38 +0200 Subject: [PATCH] feat: the WF engine should resolves escaped variables * feat: handle escape variable in exchange protocol * feat: handle escape variable in auto-completion * feat: state support dot variable --- src/ui/src/builder/BuilderTemplateInput.vue | 16 ++++++++++--- src/ui/src/renderer/useEvaluator.ts | 12 ++++++++-- src/writer/core.py | 26 ++++++++++++++------- tests/backend/test_core.py | 8 ++++--- 4 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/ui/src/builder/BuilderTemplateInput.vue b/src/ui/src/builder/BuilderTemplateInput.vue index 0605d78cd..1cc5ab0c8 100644 --- a/src/ui/src/builder/BuilderTemplateInput.vue +++ b/src/ui/src/builder/BuilderTemplateInput.vue @@ -114,7 +114,9 @@ const handleComplete = (selectedText) => { const full = getPath(text); if (full === null) return; const keyword = full.at(-1); - const replaced = text.replace(new RegExp(keyword + "$"), selectedText); + const regexKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "$"; // escape the keyword to handle properly on a regex + const replaced = text.replace(new RegExp(regexKeyword), selectedText); + newValue = replaced + newValue.slice(selectionEnd); emit("input", { target: { value: newValue } }); emit("update:value", newValue); @@ -144,6 +146,13 @@ const getPath = (text) => { return raw.split("."); }; +/** + * Escape a key to support the "." and "\" in a state variable + */ +const escapeVariable = (key) => { + return key.replace("\\", "\\\\").replace(".", "\\."); +}; + const handleInput = (ev) => { emit("input", ev); emit("update:value", ev.target.value); @@ -168,7 +177,7 @@ const showAutocomplete = () => { const allOptions = Object.entries(_get(ss.getUserState(), path) ?? {}).map( ([key, val]) => ({ - text: key, + text: escapeVariable(key), type: typeToString(val), }), ); @@ -196,8 +205,9 @@ function closeAutocompletion() { closeAutocompletionJob = setTimeout(() => { autocompleteOptions.value = []; closeAutocompletionJob = null; - }, 100); + }, 300); } + function abortClosingAutocompletion() { if (!closeAutocompletionJob) return; clearTimeout(closeAutocompletionJob); diff --git a/src/ui/src/renderer/useEvaluator.ts b/src/ui/src/renderer/useEvaluator.ts index 69c6199b8..ca2a1a1a1 100644 --- a/src/ui/src/renderer/useEvaluator.ts +++ b/src/ui/src/renderer/useEvaluator.ts @@ -17,9 +17,15 @@ export function useEvaluator(wf: Core) { s = ""; let level = 0; - for (let i = 0; i < expr.length; i++) { + let i = 0 + while (i < expr.length) { const c = expr.charAt(i); - if (c == ".") { + if (c == "\\") { + if (i + 1 < expr.length) { + s += expr.charAt(i + 1); + i++; + } + } else if (c == ".") { if (level == 0) { accessors.push(s); s = ""; @@ -44,6 +50,8 @@ export function useEvaluator(wf: Core) { } else { s += c; } + + i++ } if (s) { diff --git a/src/writer/core.py b/src/writer/core.py index 23a2f272e..ac9573dab 100644 --- a/src/writer/core.py +++ b/src/writer/core.py @@ -874,7 +874,7 @@ def calculated_property(self, for p in path_list: state_proxy = self._state_proxy - path_parts = p.split(".") + path_parts = parse_state_variable_expression(p) for i, path_part in enumerate(path_parts): if i == len(path_parts) - 1: local_mutation = MutationSubscription('property', p, handler, self, property_name) @@ -1515,28 +1515,36 @@ def parse_expression(self, expr: str, instance_path: Optional[InstancePath] = No s = "" level = 0 - for c in expr: - if c == ".": + i = 0 + while i < len(expr): + character = expr[i] + if character == "\\": + if i + 1 < len(expr): + s += expr[i + 1] + i += 1 + elif character == ".": if level == 0: accessors.append(s) s = "" else: - s += c - elif c == "[": + s += character + elif character == "[": if level == 0: accessors.append(s) s = "" else: - s += c + s += character level += 1 - elif c == "]": + elif character == "]": level -= 1 if level == 0: s = str(self.evaluate_expression(s, instance_path)) else: - s += c + s += character else: - s += c + s += character + + i += 1 if s: accessors.append(s) diff --git a/tests/backend/test_core.py b/tests/backend/test_core.py index adfc74300..bbbf262eb 100644 --- a/tests/backend/test_core.py +++ b/tests/backend/test_core.py @@ -56,7 +56,8 @@ "counter": 4, "_private": 3, # Used as an example of something unserialisable yet pickable - "_private_unserialisable": np.array([[1+2j, 2, 3+3j]]) + "_private_unserialisable": np.array([[1+2j, 2, 3+3j]]), + "a.b": 3 } simple_dict = {"items": { @@ -157,7 +158,8 @@ def test_apply_mutation_marker(self) -> None: '+interests': ['lamps', 'cars'], '+name': 'Robert', '+state\\.with\\.dots': None, - '+utfࠀ': 23 + '+utfࠀ': 23, + '+a\.b': 3 } self.sp_simple_dict.apply_mutation_marker() @@ -1135,6 +1137,7 @@ def test_evaluate_expression(self) -> None: assert e.evaluate_expression("features.eyes", instance_path) == "green" assert e.evaluate_expression("best_feature", instance_path) == "eyes" assert e.evaluate_expression("features[best_feature]", instance_path) == "green" + assert e.evaluate_expression("a\.b", instance_path) == 3 def test_get_context_data_should_return_the_target_of_event(self) -> None: """ @@ -1186,7 +1189,6 @@ def test_get_context_data_should_return_the_repeater_position_and_the_target_ins assert context.get("item") == "b" assert context.get("value") == "B" - class TestSessionManager: sm = SessionManager()