diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index 4cb6cedc60..414179753c 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -1165,3 +1165,40 @@ [class current-class] (str (if (some? class) (str class " ") "") current-class)) + + +(defn nth-index-of* + "Finds the nth occurrence of `char` in `string`, searching either forward or backward. + `dir` must be :forward (left to right) or :backward (right to left). + Returns the absolute index of the match, or nil if fewer than n occurrences exist." + [string char n dir] + (loop [s string + offset 0 + cnt 1] + (let [index (case dir + :forward (str/index-of s char) + :backward (str/last-index-of s char))] + (cond + (nil? index) nil + (= cnt n) (case dir + :forward (+ index offset) + :backward index) + :else (case dir + :forward (recur (str/slice s (inc index)) + (+ offset index 1) + (inc cnt)) + :backward (recur (str/slice s 0 index) + offset + (inc cnt))))))) + +(defn nth-index-of + "Returns the index of the nth occurrence of `char` in `string`, searching left to right. + Returns nil if fewer than n occurrences exist." + [string char n] + (nth-index-of* string char n :forward)) + +(defn nth-last-index-of + "Returns the index of the nth occurrence of `char` in `string`, searching right to left. + Returns nil if fewer than n occurrences exist." + [string char n] + (nth-index-of* string char n :backward)) \ No newline at end of file diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index 14da5a23b0..4b4314d8db 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -640,47 +640,27 @@ italic? (assoc :style "italic"))))) -;;;;;; combobox token parsing +;;;;;; Combobox token parsing (defn inside-ref? + "Returns true if `position` in `value` is inside an open reference block (i.e. after a `{` + that has no matching `}` to its left). + A reference block is considered open when the last `{` appears after the last `}`, + or when there is a `{` but no `}` at all to the left of `position`." [value position] - (let [left-part (str/slice value 0 position) - last-index-open (str/last-index-of left-part "{") - last-index-close (str/last-index-of left-part "}")] - (cond - (and (nil? last-index-open) (nil? last-index-close)) false - (and (nil? last-index-open) (some? last-index-close)) false - (and (some? last-index-open) (nil? last-index-close)) true - :else - (< last-index-close last-index-open)))) + (let [left (str/slice value 0 position) + last-open (str/last-index-of left "{") + last-close (str/last-index-of left "}")] + (and (some? last-open) + (or (nil? last-close) + (< last-close last-open))))) -(defn nth-last-index-of - [string char n] - (loop [string' string - count 1] - (let [index (str/last-index-of string' char)] - (cond - (nil? index) nil - (= count n) index - :else (recur (str/slice string' 0 index) - (inc count)))))) - -(defn nth-index-of - [string char n] - (loop [string' string - offset 0 - count 1] - (let [index (str/index-of string' char)] - (cond - (nil? index) nil - (= count n) (+ index offset) - :else (recur (str/slice string' (inc index)) - (+ offset index 1) - (inc count)))))) - -(defn block-open-start +(defn- block-open-start + "Returns the index of the leftmost `{` in the run of consecutive `{` characters + that contains the last `{` before `position` in `value`. + Used to find where a reference block truly starts when multiple braces are stacked." [value position] - (let [left (str/slice value 0 position) + (let [left (str/slice value 0 position) last-open (str/last-index-of left "{")] (loop [i last-open] (if (and i @@ -689,64 +669,71 @@ (recur (dec i)) i)))) -(defn start-ref-position +(defn- start-ref-position + "Returns the position where the current token (reference candidate) starts, + relative to `position` in `value`. + The start is determined by whichever comes last: the opening `{` of the current + reference block or the character after the last space before `position`." [value position] - (let [left-part (str/slice value 0 position) - open-pos (block-open-start value position) - space-pos (str/last-index-of left-part " ") - space-pos (when space-pos (+ 1 space-pos)) - first-position (->> [open-pos space-pos] - (remove nil?) - (sort) - (last))] - first-position)) - -(defn start-ref-position-inside-ref - [value position] - (let [left-part (str/slice value 0 position) - last-index-open (nth-last-index-of left-part "{" 1)] - last-index-open)) - -(defn end-ref-position-inside-ref - [value position] - (let [right-part (str/slice value position) - first-index-close (nth-index-of right-part "}" 1)] - first-index-close)) + (let [left (str/slice value 0 position) + open-pos (block-open-start value position) + space-pos (some-> (str/last-index-of left " ") inc)] + (->> [open-pos space-pos] + (remove nil?) + sort + last))) (defn inside-closed-ref? + "Returns true if `position` falls inside a complete (closed) reference block, + i.e. there is a `{` to the left and a `}` to the right with no spaces between + either delimiter and the position. + Returns nil (falsy) when not inside a closed reference." [value position] - (let [left-part (str/slice value 0 position) - open-pos (nth-last-index-of left-part "{" 1) - spaces-pos-before (nth-last-index-of left-part " " 1) - right-part (str/slice value position) - close-pos (nth-index-of right-part "}" 1) - spaces-pos-after (nth-index-of right-part " " 1) - open-after-space? (or (nil? spaces-pos-before) - (> open-pos spaces-pos-before)) - close-before-space? (or (nil? spaces-pos-after) - (< close-pos spaces-pos-after))] - (and open-pos - close-pos - open-after-space? - close-before-space?))) + (let [left (str/slice value 0 position) + right (str/slice value position) + open-pos (d/nth-last-index-of left "{" 1) + close-pos (d/nth-index-of right "}" 1) + last-space-left (d/nth-last-index-of left " " 1) + first-space-right (d/nth-index-of right " " 1)] + (boolean + (and open-pos + close-pos + (or (nil? last-space-left) (> open-pos last-space-left)) + (or (nil? first-space-right) (< close-pos first-space-right)))))) + +(defn- build-result + "Builds the result map for `insert-ref` by replacing the substring of `value` + between `prefix-end` and `suffix-start` with a formatted reference `{name}`. + Returns a map with: + :value — the updated string + :cursor — the index immediately after the inserted reference" + [value prefix-end suffix-start name] + (let [ref (str "{" name "}") + first-part (str/slice value 0 prefix-end) + second-part (str/slice value suffix-start)] + {:value (str first-part ref second-part) + :cursor (+ (count first-part) (count ref))})) (defn insert-ref + "Inserts a reference `{name}` into `value` at `position`, respecting the context: + + - Outside any reference block: inserts `{name}` at the cursor position. + - Inside an open reference block (no closing `}`): replaces from the block's + start up to the cursor with `{name}`. + - Inside a closed reference block (has both `{` and `}`): replaces the entire + existing reference with `{name}`. + + Returns a map with: + :value — the resulting string after insertion + :cursor — the index immediately after the inserted reference" [value position name] - (let [reference (str "{" name "}")] - (if (inside-ref? value position) - (if (inside-closed-ref? value position) - (let [first-part (str/slice value 0 (start-ref-position-inside-ref value position)) - end-position (+ position (end-ref-position-inside-ref value position) 1) - second-part (str/slice value end-position)] - {:value (str first-part reference second-part) - :cursor (+ (count first-part) (count reference))}) + (cond + (inside-ref? value position) + (if (inside-closed-ref? value position) + (let [open-pos (d/nth-last-index-of (str/slice value 0 position) "{" 1) + close-pos (+ position (d/nth-index-of (str/slice value position) "}" 1) 1)] + (build-result value open-pos close-pos name)) + (build-result value (start-ref-position value position) position name)) - (let [first-part (str/slice value 0 (start-ref-position value position)) - second-part (str/slice value position)] - {:value (str first-part reference second-part) - :cursor (+ (count first-part) (count reference))})) - - (let [first-part (str/slice value 0 position) - second-part (str/slice value position)] - {:value (str first-part reference second-part) - :cursor (+ position (count reference))})))) + :else + (build-result value position position name))) \ No newline at end of file diff --git a/common/test/common_tests/data_test.cljc b/common/test/common_tests/data_test.cljc index f2885c07f7..c4cbe4c100 100644 --- a/common/test/common_tests/data_test.cljc +++ b/common/test/common_tests/data_test.cljc @@ -113,3 +113,27 @@ (t/is (= (d/reorder v 3 -1) ["d" "a" "b" "c"])) (t/is (= (d/reorder v 5 -1) ["d" "a" "b" "c"])) (t/is (= (d/reorder v -1 5) ["b" "c" "d" "a"])))) + +(t/deftest nth-last-index-of-test + (t/is (= (d/nth-last-index-of "" "*" 1) nil)) + (t/is (= (d/nth-last-index-of "*abc" "*" 1) 0)) + (t/is (= (d/nth-last-index-of "**abc" "*" 2) 0)) + (t/is (= (d/nth-last-index-of "abc*def*ghi" "*" 3) nil)) + (t/is (= (d/nth-last-index-of "" "*" 2) nil)) + (t/is (= (d/nth-last-index-of "abc*" "*" 1) 3)) + (t/is (= (d/nth-last-index-of "abc*" "*" 2) nil)) + (t/is (= (d/nth-last-index-of "*abc[*" "*" 1) 5)) + (t/is (= (d/nth-last-index-of "abc*def*ghi" "*" 1) 7)) + (t/is (= (d/nth-last-index-of "abc*def*ghi" "*" 2) 3))) + +(t/deftest nth-index-of-test + (t/is (= (d/nth-index-of "" "*" 1) nil)) + (t/is (= (d/nth-index-of "" "*" 2) nil)) + (t/is (= (d/nth-index-of "abc*" "*" 1) 3)) + (t/is (= (d/nth-index-of "abc*" "*" 1) 3)) + (t/is (= (d/nth-index-of "abc**" "*" 2) 4)) + (t/is (= (d/nth-index-of "abc*" "*" 2) nil)) + (t/is (= (d/nth-index-of "*abc[*" "*" 1) 0)) + (t/is (= (d/nth-index-of "abc*def*ghi" "*" 1) 3)) + (t/is (= (d/nth-index-of "abc*def*ghi" "*" 2) 7)) + (t/is (= (d/nth-index-of "abc*def*ghi" "*" 3) nil))) diff --git a/common/test/common_tests/types/token_test.cljc b/common/test/common_tests/types/token_test.cljc index bbd240078e..2cde8166cf 100644 --- a/common/test/common_tests/types/token_test.cljc +++ b/common/test/common_tests/types/token_test.cljc @@ -69,31 +69,27 @@ (t/is (= (cto/insert-ref "{tok {en2}" 6 "token1") {:value "{tok {token1}" :cursor 13})) (t/is (= (cto/insert-ref "{tok en2}" 5 "token1") - {:value "{tok {token1}en2}" :cursor 13})))) + {:value "{tok {token1}en2}" :cursor 13}))) -;; TODO: pasar a common data -(t/deftest nth-last-index-of-test - (t/is (= (cto/nth-last-index-of "" "*" 1) nil)) - (t/is (= (cto/nth-last-index-of "" "*" 2) nil)) - (t/is (= (cto/nth-last-index-of "abc*" "*" 1) 3)) - (t/is (= (cto/nth-last-index-of "abc*" "*" 2) nil)) - (t/is (= (cto/nth-last-index-of "*abc[*" "*" 1) 5)) - (t/is (= (cto/nth-last-index-of "abc*def*ghi" "*" 1) 7)) - (t/is (= (cto/nth-last-index-of "abc*def*ghi" "*" 2) 3))) - -;; TODO: pasar a common data -(t/deftest nth-index-of-test - (t/is (= (cto/nth-index-of "" "*" 1) nil)) - (t/is (= (cto/nth-index-of "" "*" 2) nil)) - (t/is (= (cto/nth-index-of "abc*" "*" 1) 3)) - (t/is (= (cto/nth-index-of "abc*" "*" 2) nil)) - (t/is (= (cto/nth-index-of "*abc[*" "*" 1) 0)) - (t/is (= (cto/nth-index-of "abc*def*ghi" "*" 1) 3)) - (t/is (= (cto/nth-index-of "abc*def*ghi" "*" 2) 7))) + (t/testing "edge cases" + (t/is (= (cto/insert-ref "" 0 "x") + {:value "{x}" :cursor 3})) + (t/is (= (cto/insert-ref "abc" 3 "x") + {:value "abc{x}" :cursor 6})) + (t/is (= (cto/insert-ref "{token2}" 0 "x") + {:value "{x}{token2}" :cursor 3})) + (t/is (= (cto/insert-ref "abc" 3 "") + {:value "abc{}" :cursor 5})) + (t/is (= (cto/insert-ref "{a} {b}" 4 "x") + {:value "{a} {x}{b}" :cursor 7})))) (t/deftest inside-ref (t/is (= (cto/inside-ref? "" 1) false)) (t/is (= (cto/inside-ref? "AAA " 4) false)) + (t/is (= (cto/inside-ref? "{abc" 0) false)) + (t/is (= (cto/inside-ref? "{abc}" 5) false)) + (t/is (= (cto/inside-ref? "{a}{b}" 6) false)) + (t/is (= (cto/inside-ref? "{a{b" 4) true)) (t/is (= (cto/inside-ref? "abc{" 4) true)) (t/is (= (cto/inside-ref? "abc}" 4) false)) (t/is (= (cto/inside-ref? "{abc[}" 1) true)) @@ -101,8 +97,12 @@ (t/is (= (cto/inside-ref? "abc {def]ghi" 8) true))) (t/deftest inside-closed-ref - (t/is (= (cto/inside-closed-ref? "" 1) nil)) + (t/is (= (cto/inside-closed-ref? "" 1) false)) (t/is (= (cto/inside-closed-ref? "{abc}" 1) true)) (t/is (= (cto/inside-closed-ref? "abc {def}ghi" 5) true)) (t/is (= (cto/inside-closed-ref? "abc {def}ghi" 8) true)) - (t/is (= (cto/inside-closed-ref? "abc {def}ghi" 10) nil))) \ No newline at end of file + (t/is (= (cto/inside-closed-ref? "abc {def}ghi" 10) false)) + (t/is (= (cto/inside-closed-ref? "{abc}" 0) false)) + (t/is (= (cto/inside-closed-ref? "{abc}" 5) false)) + (t/is (= (cto/inside-closed-ref? "{ab cd}" 3) false)) + (t/is (= (cto/inside-closed-ref? "{a}{bc}" 5) true))) \ No newline at end of file diff --git a/frontend/playwright/ui/specs/tokens/apply.spec.js b/frontend/playwright/ui/specs/tokens/apply.spec.js index 0bbecffc0c..cbcdf03752 100644 --- a/frontend/playwright/ui/specs/tokens/apply.spec.js +++ b/frontend/playwright/ui/specs/tokens/apply.spec.js @@ -86,8 +86,7 @@ test.describe("Tokens: Apply token", () => { // Change token from dropdown const brTokenOptionXl = borderRadiusSection - .getByRole("option", { name: "borderRadius.xl" }) - .getByLabel("borderRadius.xl"); + .getByRole("option", { name: "borderRadius.xl" }); await expect(brTokenOptionXl).toBeVisible(); await brTokenOptionXl.click(); diff --git a/frontend/playwright/ui/specs/tokens/crud.spec.js b/frontend/playwright/ui/specs/tokens/crud.spec.js index b81b7e1df0..6d8d48414f 100644 --- a/frontend/playwright/ui/specs/tokens/crud.spec.js +++ b/frontend/playwright/ui/specs/tokens/crud.spec.js @@ -85,7 +85,7 @@ test.describe("Tokens - creation", () => { await submitButton.click(); await expect( - tokensTabPanel.getByRole("button", { name: "my-token" }), + tokensTabPanel.getByRole('checkbox', { name: 'my-token' }), ).toBeEnabled(); // Create second token referencing the first one using the combobox options