mirror of
https://github.com/tridactyl/tridactyl.git
synced 2026-09-10 07:16:33 -04:00
Swap {} for |{ }| blocks
Makes much more JS compatible without changes
This commit is contained in:
parent
00b1445575
commit
8735a6a115
|
|
@ -18,6 +18,27 @@ test.each([
|
|||
expect([...rcFileToExCmds(rc)]).toEqual(expected)
|
||||
})
|
||||
|
||||
test("groups multiline v2 blocks with explicit delimiters", () => {
|
||||
expect([
|
||||
...rcFileToExCmds("set exversion 2\nbind x |{\necho one\necho two\n}|"),
|
||||
]).toEqual([
|
||||
"set exversion 2",
|
||||
{
|
||||
source: "bind x |{\necho one\necho two\n}|",
|
||||
exversion: 2,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test.each([
|
||||
["bind x |{\necho one", "incomplete"],
|
||||
["bind x }|", "invalid"],
|
||||
])("rejects %s v2 RC blocks", (source, error) => {
|
||||
expect(() => [...rcFileToExCmds(`set exversion 2\n${source}`)]).toThrow(
|
||||
`${error} ex command`,
|
||||
)
|
||||
})
|
||||
|
||||
test("runRc updates and saves versioned config", async () => {
|
||||
await config.clear()
|
||||
jest.mocked(controller.acceptExCmd).mockImplementation(async cmd => {
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ export function getCommandlineFns(cmdline_state: {
|
|||
if (
|
||||
!command.startsWith(" ") &&
|
||||
!browser.extension.inIncognitoContext &&
|
||||
!/(^|[\s;|{}])--?private(?=$|[\s;|{}])/.test(command)
|
||||
!/(^|[\s;|])--?private(?=$|[\s;|])/.test(command)
|
||||
) {
|
||||
State.getAsync("cmdHistory").then(c => {
|
||||
cmdline_state.state.cmdHistory = c.concat([command])
|
||||
|
|
|
|||
|
|
@ -229,13 +229,15 @@ test("groups versioned programs after legacy RC commands", () => {
|
|||
expect(rc.indexOf("bind x echo legacy | command")).toBeLessThan(
|
||||
rc.indexOf("set exversion 2"),
|
||||
)
|
||||
expect(rc).toContain("bind y {\necho one\necho two\n}")
|
||||
expect(rc).toContain("bind = {\necho equals\n}")
|
||||
expect(rc).toContain("bind y |{\necho one\necho two\n}|")
|
||||
expect(rc).toContain("bind = |{\necho equals\n}|")
|
||||
expect(rc).toContain("set exversion 1")
|
||||
tri.config.USERCONFIG.nmaps = {
|
||||
";": { source: "echo unsafe", exversion: 2 },
|
||||
for (const key of [";", "\\|{", "\\}|"]) {
|
||||
tri.config.USERCONFIG.nmaps = {
|
||||
[key]: { source: "echo unsafe", exversion: 2 },
|
||||
}
|
||||
expect(() => tri.config.parseConfig()).toThrow("safely export")
|
||||
}
|
||||
expect(() => tri.config.parseConfig()).toThrow("safely export")
|
||||
} finally {
|
||||
tri.config.USERCONFIG.nmaps = nmaps
|
||||
tri.config.USERCONFIG.exversion = exversion
|
||||
|
|
|
|||
|
|
@ -18,12 +18,24 @@
|
|||
*/
|
||||
import * as R from "ramda"
|
||||
import * as binding from "@src/lib/binding"
|
||||
import { ExCommand, formatExProgram, isExProgram } from "@src/lib/excmd"
|
||||
import {
|
||||
EX_BLOCK_CLOSE,
|
||||
EX_BLOCK_OPEN,
|
||||
ExCommand,
|
||||
formatExProgram,
|
||||
isExProgram,
|
||||
} from "@src/lib/excmd"
|
||||
import * as platform from "@src/lib/platform"
|
||||
import { DeepPartial } from "tsdef"
|
||||
|
||||
const v2Syntax = [".|", "&&", "||", "|", ";", EX_BLOCK_OPEN, EX_BLOCK_CLOSE]
|
||||
const escapableV2Syntax = [".|", "|", ";", EX_BLOCK_OPEN, EX_BLOCK_CLOSE]
|
||||
const assertV2Argument = (value: string) => {
|
||||
if (/[\s'"]|^(?:\.\||&&|\|\||[|;{}])$/.test(value))
|
||||
if (
|
||||
/[\s'"]/.test(value) ||
|
||||
v2Syntax.includes(value) ||
|
||||
(value[0] === "\\" && escapableV2Syntax.includes(value.slice(1)))
|
||||
)
|
||||
throw new Error(`Cannot safely export dialect 2 argument: ${value}`)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ export async function acceptExCmd(
|
|||
func !== stored_excmds[""].fillcmdline_tmp &&
|
||||
func !== stored_excmds[""].fillcmdline_nofocus &&
|
||||
func !== stored_excmds[""].updatecheck &&
|
||||
!/(^|[\s;|{}])-private(?=$|[\s;|{}])/.test(exstr)
|
||||
!/(^|[\s;|])--?private(?=$|[\s;|])/.test(exstr)
|
||||
) {
|
||||
lastExUpdate = State.getAsync("last_ex_str").then(
|
||||
last_ex_str => {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ export interface ExProgram {
|
|||
exversion: 2
|
||||
}
|
||||
|
||||
export const EX_BLOCK_OPEN = "|{"
|
||||
export const EX_BLOCK_CLOSE = "}|"
|
||||
|
||||
export const EX_CANCELLED = Object.freeze({
|
||||
__tridactylExOutcome: "cancelled" as const,
|
||||
})
|
||||
|
|
@ -36,7 +39,7 @@ export const formatExProgram = (command: ExCommand) => {
|
|||
if (!isExProgram(command)) return command
|
||||
const prefix = command.source.startsWith("\n") ? "" : "\n"
|
||||
const suffix = command.source.endsWith("\n") ? "" : "\n"
|
||||
return `{${prefix}${command.source}${suffix}}`
|
||||
return `${EX_BLOCK_OPEN}${prefix}${command.source}${suffix}${EX_BLOCK_CLOSE}`
|
||||
}
|
||||
|
||||
export function joinExCommand(parts: ExCommand[]): ExCommand {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,9 @@ test.each([
|
|||
"echo a |b",
|
||||
"echo C:\\windows\\etc",
|
||||
"echo /a\\|b/",
|
||||
"echo {| b",
|
||||
"echo a|{b",
|
||||
"echo a}|b",
|
||||
"echo a|{b}|c",
|
||||
])("does not split protected or non-standalone operators in %s", source => {
|
||||
expect(shape(source)).toEqual([["text", source, undefined]])
|
||||
expect(parseStructure(source).status).toBe("complete")
|
||||
|
|
@ -46,9 +48,9 @@ test.each([
|
|||
|
||||
test("only backslash protects and is removed from standalone DSL syntax", async () => {
|
||||
const run = jest.fn()
|
||||
await evaluate("echo \\| \\&& \\|| \\.| \\; \\{ \\} \\{\\}", run)
|
||||
await evaluate("echo \\| \\&& \\|| \\.| \\; \\|{ \\}| \\{ \\}", run)
|
||||
expect(run).toHaveBeenCalledWith(
|
||||
"echo | \\&& \\|| .| ; { } {}",
|
||||
"echo | \\&& \\|| .| ; |{ }| \\{ \\}",
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
|
|
@ -89,12 +91,12 @@ test("protects operators in whole-line comments and separates lines", () => {
|
|||
})
|
||||
|
||||
test("parses nested blocks independently", () => {
|
||||
const source = "bind x { hint | tabopen } ; echo done"
|
||||
const source = "bind x |{ hint | tabopen }| ; echo done"
|
||||
expect(shape(source)).toEqual([
|
||||
["text", "bind x ", undefined],
|
||||
[
|
||||
"block",
|
||||
"{ hint | tabopen }",
|
||||
"|{ hint | tabopen }|",
|
||||
[
|
||||
["text", " hint ", undefined],
|
||||
["operator", "|", undefined],
|
||||
|
|
@ -107,8 +109,14 @@ test("parses nested blocks independently", () => {
|
|||
])
|
||||
})
|
||||
|
||||
test("allows an escaped closing delimiter inside a block", async () => {
|
||||
const run = jest.fn()
|
||||
await evaluate("|{ echo \\}| }|", run)
|
||||
expect(run).toHaveBeenCalledWith("echo }|", false, undefined, undefined)
|
||||
})
|
||||
|
||||
test("reports an unterminated block as incomplete", () =>
|
||||
expect(parseStructure("bind x { echo done").status).toBe("incomplete"))
|
||||
expect(parseStructure("bind x |{ echo done").status).toBe("incomplete"))
|
||||
|
||||
test.each(["echo a |", "echo a .|", "echo a ;"])(
|
||||
"reports a missing right operand in %s",
|
||||
|
|
@ -119,8 +127,8 @@ test.each([
|
|||
"| echo a",
|
||||
"echo a | | echo b",
|
||||
"echo a ; ; echo b",
|
||||
"echo a }",
|
||||
"{ echo a | }",
|
||||
"echo a }|",
|
||||
"|{ echo a | }|",
|
||||
"js <<JS console.log(1)\necho done",
|
||||
"js <<JS console.log(1)\rmore JS",
|
||||
"js <<JS console.log(1)\r",
|
||||
|
|
@ -145,7 +153,7 @@ test("evaluates pipes and sequences in order", async () => {
|
|||
|
||||
test("ignores leading colons on every command", async () => {
|
||||
const run = jest.fn(source => source)
|
||||
await evaluate(":one\n{ :::two } ; :echo :::argument", run)
|
||||
await evaluate(":one\n|{ :::two }| ; :echo :::argument", run)
|
||||
expect(run.mock.calls.map(call => call[0])).toEqual([
|
||||
"one",
|
||||
"two",
|
||||
|
|
@ -357,7 +365,7 @@ test("maps a command with arguments and pipes the collected results", async () =
|
|||
test("maps a multi-stage block", () =>
|
||||
expect(
|
||||
evaluate(
|
||||
"values .| { double | stringify }",
|
||||
"values .| |{ double | stringify }|",
|
||||
(command, _piped, input) => {
|
||||
if (command === "values") return [1, 2]
|
||||
if (command === "double") return input * 2
|
||||
|
|
@ -368,7 +376,7 @@ test("maps a multi-stage block", () =>
|
|||
|
||||
test("supports nested block maps", () =>
|
||||
expect(
|
||||
evaluate("matrix .| { pass .| double }", (command, _piped, input) => {
|
||||
evaluate("matrix .| |{ pass .| double }|", (command, _piped, input) => {
|
||||
if (command === "matrix")
|
||||
return [
|
||||
[1, 2],
|
||||
|
|
@ -473,7 +481,7 @@ test("continues sequences after a rejected pipeline", async () => {
|
|||
expect(run.mock.calls.map(call => call[0])).toEqual(["a", "c"])
|
||||
})
|
||||
|
||||
test.each(["a | cancel | b ; c", "a | { cancel | b } ; c"])(
|
||||
test.each(["a | cancel | b ; c", "a | |{ cancel | b }| ; c"])(
|
||||
"cancellation stops the whole program in %s",
|
||||
async source => {
|
||||
const run = jest.fn(command =>
|
||||
|
|
@ -517,7 +525,9 @@ test("evaluates nested standalone blocks with pipeline input", async () => {
|
|||
calls.push([source, piped, value])
|
||||
return piped ? `${source}(${value})` : source
|
||||
})
|
||||
await expect(evaluate("a | { b | { c } }", run)).resolves.toBe("c(b(a))")
|
||||
await expect(evaluate("a | |{ b | |{ c }| }|", run)).resolves.toBe(
|
||||
"c(b(a))",
|
||||
)
|
||||
expect(calls).toEqual([
|
||||
["a", false, undefined],
|
||||
["b", true, "a"],
|
||||
|
|
@ -527,7 +537,7 @@ test("evaluates nested standalone blocks with pipeline input", async () => {
|
|||
|
||||
test("passes a trailing block as a versioned program argument", async () => {
|
||||
const run = jest.fn()
|
||||
await evaluate("bind x { a\n# keep this comment\nb }", run)
|
||||
await evaluate("bind x |{ a\n# keep this comment\nb }|", run)
|
||||
expect(run).toHaveBeenCalledWith("bind x", false, undefined, {
|
||||
source: " a\n# keep this comment\nb ",
|
||||
exversion: 2,
|
||||
|
|
@ -605,17 +615,28 @@ test("formats blocks containing whole-line comments safely", async () => {
|
|||
|
||||
test("rejects unsupported nested syntax before executing", async () => {
|
||||
const run = jest.fn()
|
||||
await expect(evaluate("a | { bind x { b } }", run)).rejects.toThrow(
|
||||
await expect(evaluate("a | |{ bind x |{ b }| }|", run)).rejects.toThrow(
|
||||
"Unsupported",
|
||||
)
|
||||
expect(run).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test.each([
|
||||
"bind { a } trailing",
|
||||
"bind x { a } { b }",
|
||||
"a | bind x { b }",
|
||||
"bind x { a } <<JS\nb\nJS",
|
||||
"bind |{ a }| trailing",
|
||||
"bind x |{ a }| |{ b }|",
|
||||
"a | bind x |{ b }|",
|
||||
"bind x |{ a }| <<JS\nb\nJS",
|
||||
])("rejects ambiguous block arguments in %s", source =>
|
||||
expect(evaluate(source, jest.fn())).rejects.toThrow("Unsupported"),
|
||||
)
|
||||
|
||||
test.each([
|
||||
"js if (ready) { work() }",
|
||||
"js const value = { key: 1 }",
|
||||
"js let url; { let node = find() }",
|
||||
"echo { unmatched } braces",
|
||||
])("treats ordinary braces as command text in %s", async source => {
|
||||
const run = jest.fn()
|
||||
await evaluate(source, run)
|
||||
expect(run).toHaveBeenCalledWith(source, false, undefined, undefined)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
import { ExProgram, isExCancelled, stripLeadingColons } from "@src/lib/excmd"
|
||||
import {
|
||||
EX_BLOCK_CLOSE,
|
||||
EX_BLOCK_OPEN,
|
||||
ExProgram,
|
||||
isExCancelled,
|
||||
stripLeadingColons,
|
||||
} from "@src/lib/excmd"
|
||||
|
||||
const operators = [".|", "|", ";"] as const
|
||||
|
||||
|
|
@ -20,13 +26,20 @@ export type ExPart =
|
|||
| ({ type: "operator"; operator: ExOperator } & Span)
|
||||
| ({ type: "comment" } & Span)
|
||||
| ({ type: "heredoc"; bodyStart: number; bodyEnd: number } & Span)
|
||||
| ({ type: "block"; body: ExStructure } & Span)
|
||||
| ({
|
||||
type: "block"
|
||||
body: ExStructure
|
||||
bodyStart: number
|
||||
bodyEnd: number
|
||||
} & Span)
|
||||
|
||||
interface ParseResult extends ExStructure {
|
||||
end: number
|
||||
bodyEnd?: number
|
||||
}
|
||||
|
||||
const boundaries = " \t\r\n"
|
||||
const blockDelimiters = [EX_BLOCK_OPEN, EX_BLOCK_CLOSE]
|
||||
|
||||
const status = (
|
||||
invalid: boolean,
|
||||
|
|
@ -54,13 +67,14 @@ function escapedSyntaxEnd(source: string, index: number) {
|
|||
isBoundary(source[index + candidate.length + 1]),
|
||||
)
|
||||
if (operator) return index + operator.length + 1
|
||||
const escaped = source[index + 1]
|
||||
if (
|
||||
(escaped === "{" || escaped === "}") &&
|
||||
isBraceBoundary(source, index - 1) &&
|
||||
isBraceBoundary(source, index + 2)
|
||||
const blockDelimiter = blockDelimiters.find(
|
||||
delimiter =>
|
||||
source.startsWith(delimiter, index + 1) &&
|
||||
isBoundary(source[index - 1]) &&
|
||||
isBoundary(source[index + delimiter.length + 1]),
|
||||
)
|
||||
return index + 2
|
||||
if (blockDelimiter) return index + blockDelimiter.length + 1
|
||||
const escaped = source[index + 1]
|
||||
if (
|
||||
(escaped === "#" || escaped === '"') &&
|
||||
isWholeLineComment(source, index)
|
||||
|
|
@ -69,18 +83,12 @@ function escapedSyntaxEnd(source: string, index: number) {
|
|||
return undefined
|
||||
}
|
||||
|
||||
function isBraceBoundary(source: string, index: number) {
|
||||
const character = source[index]
|
||||
return (
|
||||
isBoundary(character) ||
|
||||
"{}".includes(character) ||
|
||||
(character === "\\" && "{}".includes(source[index + 1]))
|
||||
)
|
||||
}
|
||||
|
||||
function isStandaloneBrace(source: string, index: number) {
|
||||
return (
|
||||
isBraceBoundary(source, index - 1) && isBraceBoundary(source, index + 1)
|
||||
function blockDelimiterAt(source: string, index: number) {
|
||||
if (!isBoundary(source[index - 1])) return undefined
|
||||
return blockDelimiters.find(
|
||||
delimiter =>
|
||||
source.startsWith(delimiter, index) &&
|
||||
isBoundary(source[index + delimiter.length]),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -206,14 +214,18 @@ function parseRange(
|
|||
textStart = index + 1
|
||||
continue
|
||||
}
|
||||
if (character === "{" && isStandaloneBrace(source, index)) {
|
||||
const blockDelimiter = blockDelimiterAt(source, index)
|
||||
if (blockDelimiter === EX_BLOCK_OPEN) {
|
||||
text(index)
|
||||
const child = parseRange(source, index + 1, true)
|
||||
const bodyStart = index + EX_BLOCK_OPEN.length
|
||||
const child = parseRange(source, bodyStart, true)
|
||||
const end = child.end
|
||||
parts.push({
|
||||
type: "block",
|
||||
start: index,
|
||||
end,
|
||||
bodyStart,
|
||||
bodyEnd: child.bodyEnd ?? source.length,
|
||||
body: { parts: child.parts, status: child.status },
|
||||
})
|
||||
incomplete = incomplete || child.status === "incomplete"
|
||||
|
|
@ -223,7 +235,7 @@ function parseRange(
|
|||
textStart = end
|
||||
continue
|
||||
}
|
||||
if (nested && character === "}" && isStandaloneBrace(source, index)) {
|
||||
if (nested && blockDelimiter === EX_BLOCK_CLOSE) {
|
||||
text(index)
|
||||
return {
|
||||
parts,
|
||||
|
|
@ -231,11 +243,13 @@ function parseRange(
|
|||
invalid || expectation === "operand",
|
||||
incomplete,
|
||||
),
|
||||
end: index + 1,
|
||||
end: index + EX_BLOCK_CLOSE.length,
|
||||
bodyEnd: index,
|
||||
}
|
||||
}
|
||||
if (!nested && character === "}" && isStandaloneBrace(source, index)) {
|
||||
if (!nested && blockDelimiter === EX_BLOCK_CLOSE) {
|
||||
invalid = true
|
||||
index += EX_BLOCK_CLOSE.length - 1
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -340,7 +354,7 @@ function compile(
|
|||
command: blockCommand,
|
||||
piped,
|
||||
program: {
|
||||
source: source.slice(block.start + 1, block.end - 1),
|
||||
source: source.slice(block.bodyStart, block.bodyEnd),
|
||||
exversion: 2,
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -57,12 +57,12 @@ echo hello
|
|||
# bare expressions are allowed in pipes
|
||||
:js [{hello: "world"}, {mellow: "yellow"}, {hello: "universe"}] .| _.hello | filter | fillcmdline
|
||||
|
||||
# { } form blocks
|
||||
:bind ;Y {
|
||||
# |{ }| form blocks
|
||||
:bind ;Y |{
|
||||
hint -eJc img |
|
||||
_.src |
|
||||
yankimage
|
||||
}
|
||||
}|
|
||||
|
||||
# heredocs are supported for making javascript more pleasant
|
||||
:js <<JS
|
||||
|
|
|
|||
Loading…
Reference in a new issue