HS: pick regex + indices (+13 tests)
Implements cluster 19 — pick command extensions for hs-upstream-pick suite
(11/24 → 24/24, +13):
- Parser:
- pick items/item EXPR to EXPR supports `start` and `end` keywords
- pick match / pick matches accept `| <flag>` syntax after regex
- pick item N without `to` still works (single-item slice)
- Runtime:
- hs-pick-items / hs-pick-first / hs-pick-last now handle strings
(not just lists) via slice
- hs-pick-items resolves `start`/`end` sentinel strings and negative
indices (len + N) at runtime
- hs-pick-matches added (wraps regex-find-all, each match as a list)
- hs-pick-regex-pattern handles (list pat flags) form; `i` flag
transforms pattern to case-insensitive by replacing alpha chars with
[aA] character classes (Re.Pcre has no inline-flag support)
- Generator:
- extract_hs_expr now decodes JS string escape sequences (\" -> ",
\\ -> \) instead of stripping all backslashes, then re-escapes for
SX. Preserves regex escapes (\d, \s), CSS escapes, and lambda `\`
syntax for String.raw template literals while still producing
correct output for regular JS strings.
Smoke (0-195): 170/195 unchanged (no regressions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4183,7 +4183,7 @@
|
||||
(assert= (map (fn (x) (get x "name")) (eval-hs "set arr to [{name: \"b\", age: 30}, {name: \"a\", age: 20}, {name: \"c\", age: 25}] then return arr sorted by its age")) (list "a" "c" "b"))
|
||||
)
|
||||
(deftest "split by on null returns null"
|
||||
(eval-hs "set x to null then return x split by ','n")
|
||||
(eval-hs "set x to null then return x split by ','\\n")
|
||||
)
|
||||
(deftest "the result inside where refers to previous command result, not current element"
|
||||
(assert= (eval-hs "get 3 then set arr to [1, 2, 3, 4, 5] then return arr where it > the result") (list 4 5))
|
||||
@@ -9135,13 +9135,13 @@
|
||||
(assert= (eval-hs-locals "pick item 2 from arr set $test to it" (list (list (quote arr) (list 10 11 12 13 14 15 16)))) (list 12))
|
||||
)
|
||||
(deftest "can pick a single regex match"
|
||||
(assert= (eval-hs-locals "pick match of \"d+\" from haystack set window.test to it" (list (list (quote haystack) "The 32 quick brown foxes jumped 12 times over the 149 lazy dogs"))) (list "32"))
|
||||
(assert= (eval-hs-locals "pick match of \"\\d+\" from haystack set window.test to it" (list (list (quote haystack) "The 32 quick brown foxes jumped 12 times over the 149 lazy dogs"))) (list "32"))
|
||||
)
|
||||
(deftest "can pick a single regex match w/ a flag"
|
||||
(assert= (eval-hs-locals "pick match of \"t.e\" | i from haystack set window.test to it" (list (list (quote haystack) "The 32 quick brown foxes jumped 12 times over the 149 lazy dogs"))) (list "The"))
|
||||
)
|
||||
(deftest "can pick all regex matches"
|
||||
(assert= (eval-hs-locals "pick matches of \"d+\" from haystack set window.test to it" (list (list (quote haystack) "The 32 quick brown foxes jumped 12 times over the 149 lazy dogs"))) (list (list "32") (list "12") (list "149")))
|
||||
(assert= (eval-hs-locals "pick matches of \"\\d+\" from haystack set window.test to it" (list (list (quote haystack) "The 32 quick brown foxes jumped 12 times over the 149 lazy dogs"))) (list (list "32") (list "12") (list "149")))
|
||||
)
|
||||
(deftest "can pick first n items"
|
||||
(assert= (eval-hs-locals "pick first 3 of arr set $test to it" (list (list (quote arr) (list 10 20 30 40 50)))) (list 10 20 30))
|
||||
@@ -9159,7 +9159,7 @@
|
||||
(assert= (eval-hs-locals "pick last 2 of arr set $test to it" (list (list (quote arr) (list 10 20 30 40 50)))) (list 40 50))
|
||||
)
|
||||
(deftest "can pick match using 'of' syntax"
|
||||
(assert= (eval-hs-locals "pick match of \"d+\" of haystack set window.test to it" (list (list (quote haystack) "The 32 quick brown foxes"))) (list "32"))
|
||||
(assert= (eval-hs-locals "pick match of \"\\d+\" of haystack set window.test to it" (list (list (quote haystack) "The 32 quick brown foxes"))) (list "32"))
|
||||
)
|
||||
(deftest "can pick random item"
|
||||
(assert (not (nil? (eval-hs-locals "pick random of arr set $test to it" (list (list (quote arr) (list 10 20 30)))))))
|
||||
@@ -9186,7 +9186,7 @@
|
||||
(assert= (eval-hs-locals "pick items 0 to -4 from arr set $test to it" (list (list (quote arr) (list 10 11 12 13 14 15 16)))) (list 10 11 12))
|
||||
)
|
||||
(deftest "does not hang on zero-length regex matches"
|
||||
(assert (not (nil? (eval-hs-locals "pick matches of \"d*\" from haystack set window.test to it" (list (list (quote haystack) "a1b"))))))
|
||||
(assert (not (nil? (eval-hs-locals "pick matches of \"\\d*\" from haystack set window.test to it" (list (list (quote haystack) "a1b"))))))
|
||||
)
|
||||
(deftest "pick first from null returns null"
|
||||
(eval-hs "set x to null then pick first 3 from x then return it")
|
||||
@@ -9195,7 +9195,7 @@
|
||||
(eval-hs "set x to null then pick last 2 from x then return it")
|
||||
)
|
||||
(deftest "pick match from null returns null"
|
||||
(eval-hs "set x to null then pick match of \"d+\" from x then return it")
|
||||
(eval-hs "set x to null then pick match of \"\\d+\" from x then return it")
|
||||
)
|
||||
(deftest "pick random from null returns null"
|
||||
(eval-hs "set x to null then pick random from x then return it")
|
||||
|
||||
@@ -1768,14 +1768,49 @@ def _js_window_expr_to_sx(expr):
|
||||
return None
|
||||
|
||||
|
||||
def _decode_js_escapes(s):
|
||||
"""Decode JS string escape sequences.
|
||||
- \\" -> " (escaped quote)
|
||||
- \\' -> '
|
||||
- \\` -> `
|
||||
- \\\\ -> \\ (escaped backslash)
|
||||
- \\n, \\t -> space (already normalized)
|
||||
- Other \\X sequences (e.g. \\d for regex) are preserved literally,
|
||||
matching String.raw semantics for unknown escapes.
|
||||
"""
|
||||
out = []
|
||||
i = 0
|
||||
while i < len(s):
|
||||
c = s[i]
|
||||
if c == '\\' and i + 1 < len(s):
|
||||
nxt = s[i + 1]
|
||||
if nxt in ('"', "'", '`'):
|
||||
out.append(nxt)
|
||||
i += 2
|
||||
continue
|
||||
if nxt == '\\':
|
||||
out.append('\\')
|
||||
i += 2
|
||||
continue
|
||||
# Unknown escape: preserve both chars (regex \\d, CSS \\:, lambda \\ -> )
|
||||
out.append(c)
|
||||
i += 1
|
||||
continue
|
||||
out.append(c)
|
||||
i += 1
|
||||
return ''.join(out)
|
||||
|
||||
|
||||
def extract_hs_expr(raw):
|
||||
"""Clean a HS expression extracted from run() call."""
|
||||
# Remove surrounding whitespace and newlines
|
||||
expr = raw.strip().replace('\n', ' ').replace('\t', ' ')
|
||||
# Collapse multiple spaces
|
||||
expr = re.sub(r'\s+', ' ', expr)
|
||||
# Escape backslashes (preserve regex escapes like \d, CSS escapes, lambda \)
|
||||
# then escape quotes for SX string.
|
||||
# Decode JS-level escape sequences while preserving regex/CSS/lambda
|
||||
# backslashes. \" -> ", \\ -> \, \d -> \d (unchanged).
|
||||
expr = _decode_js_escapes(expr)
|
||||
# Re-escape for SX string literal: backslashes, then quotes.
|
||||
expr = expr.replace('\\', '\\\\').replace('"', '\\"')
|
||||
return expr
|
||||
|
||||
|
||||
Reference in New Issue
Block a user