mirror of
https://github.com/tridactyl/tridactyl.git
synced 2026-09-10 07:16:33 -04:00
Add filter and underscore magic lambdas
This commit is contained in:
parent
f246c5f7ee
commit
8a6eb03f41
|
|
@ -39,7 +39,7 @@ class Signature:
|
|||
# Type declaration
|
||||
if ':' in param:
|
||||
name, typ = map(str.strip, param.split(':'))
|
||||
if (typ not in ('number', 'boolean', 'string', 'string[]', 'ModeName')
|
||||
if (typ not in ('number', 'boolean', 'string', 'string[]', 'any[]', 'ModeName')
|
||||
and '|' not in typ
|
||||
and typ[0] not in ['"',"'"]
|
||||
):
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ import * as Native from "@src/lib/native"
|
|||
import * as TTS from "@src/lib/text_to_speech"
|
||||
import * as excmd_parser from "@src/parsers/exmode"
|
||||
import * as escape from "@src/lib/escape"
|
||||
import * as Collections from "@src/lib/collections"
|
||||
import semverCompare from "semver-compare"
|
||||
import * as hint_util from "@src/lib/hint_util"
|
||||
import { OpenMode } from "@src/lib/hint_util"
|
||||
|
|
@ -4158,6 +4159,27 @@ export async function composite(...cmds: string[]) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform every element of a piped array with an underscore selector.
|
||||
* `_` is identity; dotted paths select properties and throw when an
|
||||
* intermediate value is missing. In exversion 2, non-selector targets map ex
|
||||
* commands, and `array .| target` is shorthand for `array | map target`.
|
||||
* Example: `js [{url: "one"}, {url: "two"}] | map _.url`.
|
||||
*/
|
||||
//#both
|
||||
export function map(expression: string, values: any[]): any[] {
|
||||
return Collections.map(expression, values)
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep elements of a piped array whose underscore selector is truthy.
|
||||
* Example: `js [{url: "one"}, {}] | filter _.url`.
|
||||
*/
|
||||
//#both
|
||||
export function filter(expression: string, values: any[]): any[] {
|
||||
return Collections.filter(expression, values)
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape command for safe use in shell with composite. E.g: `composite js MALICIOUS_WEBSITE_FUNCTION() | shellescape | exclaim ls`
|
||||
*/
|
||||
|
|
|
|||
27
src/lib/collections.test.ts
Normal file
27
src/lib/collections.test.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { filter, map, selector } from "@src/lib/collections"
|
||||
|
||||
test.each([
|
||||
["_", { url: "one" }, { url: "one" }],
|
||||
["_.url", { url: "one" }, "one"],
|
||||
["_.author.name", { author: { name: "Olie" } }, "Olie"],
|
||||
])("applies magic selector %s", (source, value, expected) =>
|
||||
expect(selector(source)(value)).toEqual(expected),
|
||||
)
|
||||
|
||||
test("maps and filters arrays with magic selectors", () => {
|
||||
const values = [{ url: "one" }, {}, { url: "two" }]
|
||||
expect(map("_.url", filter("_.url", values))).toEqual(["one", "two"])
|
||||
})
|
||||
|
||||
test("uses direct property access semantics", () =>
|
||||
expect(() => selector("_.author.name")({})).toThrow())
|
||||
|
||||
test.each(["url", "_.url()", "_.0", "_.foo-bar"])(
|
||||
"rejects unsupported selector %s",
|
||||
source => expect(() => selector(source)).toThrow("selector"),
|
||||
)
|
||||
|
||||
test.each(["one two", { one: 1 }])("rejects non-array input", value => {
|
||||
expect(() => map("_", value as any)).toThrow("array")
|
||||
expect(() => filter("_", value as any)).toThrow("array")
|
||||
})
|
||||
23
src/lib/collections.ts
Normal file
23
src/lib/collections.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
const selectorPattern = /^_(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/
|
||||
|
||||
export const isSelector = (source: string) => selectorPattern.test(source)
|
||||
|
||||
export function selector(source: string): (value: any) => any {
|
||||
if (!selectorPattern.test(source))
|
||||
throw new Error(`Invalid selector: ${source}`)
|
||||
const path = source.split(".").slice(1)
|
||||
return value => path.reduce((current, property) => current[property], value)
|
||||
}
|
||||
|
||||
function array(values: any): any[] {
|
||||
if (!Array.isArray(values)) throw new Error("Expected an array")
|
||||
return values
|
||||
}
|
||||
|
||||
export function map(source: string, values: any[]): any[] {
|
||||
return array(values).map(selector(source))
|
||||
}
|
||||
|
||||
export function filter(source: string, values: any[]): any[] {
|
||||
return array(values).filter(selector(source))
|
||||
}
|
||||
|
|
@ -152,18 +152,37 @@ test("preserves typed values in ordinary pipes", async () => {
|
|||
expect(run.mock.calls[1][2]).toBe(value)
|
||||
})
|
||||
|
||||
test.each(["values .| double", "values | map double"])(
|
||||
"maps exactly one command in %s",
|
||||
test.each([
|
||||
["values .| double", "double"],
|
||||
["values | map double", "double"],
|
||||
["values .| _double", "_double"],
|
||||
["values | map _double", "_double"],
|
||||
])("maps exactly one command in %s", async (source, target) => {
|
||||
const run = jest.fn((command, _piped, input) =>
|
||||
command === "values" ? [1, 2, 3] : input * 2,
|
||||
)
|
||||
await expect(evaluate(source, run)).resolves.toEqual([2, 4, 6])
|
||||
expect(run.mock.calls.map(call => call[0])).toEqual([
|
||||
"values",
|
||||
target,
|
||||
target,
|
||||
target,
|
||||
])
|
||||
})
|
||||
|
||||
test.each(["values .| _.url", "values | map _.url"])(
|
||||
"maps magic selectors through the standard command in %s",
|
||||
async source => {
|
||||
const run = jest.fn((command, _piped, input) =>
|
||||
command === "values" ? [1, 2, 3] : input * 2,
|
||||
)
|
||||
await expect(evaluate(source, run)).resolves.toEqual([2, 4, 6])
|
||||
const values = [{ url: "one" }, { url: "two" }]
|
||||
const run = jest.fn((command, _piped, input) => {
|
||||
if (command === "values") return values
|
||||
if (command === "map _.url") return input.map(value => value.url)
|
||||
throw new Error(`Unexpected command: ${command}`)
|
||||
})
|
||||
await expect(evaluate(source, run)).resolves.toEqual(["one", "two"])
|
||||
expect(run.mock.calls.map(call => call[0])).toEqual([
|
||||
"values",
|
||||
"double",
|
||||
"double",
|
||||
"double",
|
||||
"map _.url",
|
||||
])
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ExProgram } from "@src/lib/excmd"
|
||||
import { isSelector } from "@src/lib/collections"
|
||||
|
||||
const operators = [".|", "&&", "||", "|", ";"] as const
|
||||
|
||||
|
|
@ -323,13 +324,20 @@ function compile(
|
|||
raw?: string,
|
||||
): ExStage {
|
||||
const mapped = /^map(?:\s+(.*))?$/.exec(command)
|
||||
if (!mapped) return { command, piped, raw }
|
||||
if (!mapped || (mapped[1] && isSelector(mapped[1])))
|
||||
return { command, piped, raw }
|
||||
if (!mapped[1]) throw new Error("map requires a command or block")
|
||||
return mapStage([commandStage(mapped[1], false, raw)], piped)
|
||||
}
|
||||
|
||||
const push = (stage: ExStage) => {
|
||||
stages.push(mapNext ? mapStage([stage], true) : stage)
|
||||
stages.push(
|
||||
mapNext
|
||||
? isSelector(stage.command)
|
||||
? { ...stage, command: `map ${stage.command}`, piped: true }
|
||||
: mapStage([stage], true)
|
||||
: stage,
|
||||
)
|
||||
mapNext = false
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue