mirror of
https://github.com/penpot/penpot.git
synced 2026-03-12 13:27:02 +00:00
♻️ Refactor new fns and add docstrings
This commit is contained in:
@@ -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))
|
||||
@@ -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)))
|
||||
@@ -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)))
|
||||
|
||||
@@ -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)))
|
||||
(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)))
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user