@weborigami/language 0.6.10 → 0.6.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/main.js +2 -4
- package/package.json +4 -4
- package/src/compiler/origami.pegjs +11 -5
- package/src/compiler/parse.js +56 -10
- package/src/compiler/parserHelpers.js +3 -4
- package/src/handlers/sh_handler.js +7 -2
- package/src/handlers/yaml_handler.js +8 -2
- package/src/project/jsGlobals.js +1 -10
- package/src/protocols/package.js +19 -10
- package/src/runtime/WatchFilesMixin.d.ts +3 -1
- package/src/runtime/WatchFilesMixin.js +7 -0
- package/src/runtime/errors.js +5 -0
- package/src/runtime/execute.js +17 -13
- package/src/runtime/ops.js +29 -4
- package/test/compiler/parse.test.js +4 -8
- package/test/runtime/ops.test.js +6 -0
package/main.js
CHANGED
|
@@ -13,11 +13,9 @@ export { default as projectRoot } from "./src/project/projectRoot.js";
|
|
|
13
13
|
export { default as projectRootFromPath } from "./src/project/projectRootFromPath.js";
|
|
14
14
|
export * as Protocols from "./src/protocols/protocols.js";
|
|
15
15
|
export { formatError, highlightError, lineInfo } from "./src/runtime/errors.js";
|
|
16
|
+
export { default as evaluate } from "./src/runtime/evaluate.js";
|
|
16
17
|
export { default as EventTargetMixin } from "./src/runtime/EventTargetMixin.js";
|
|
17
|
-
export {
|
|
18
|
-
default as evaluate,
|
|
19
|
-
default as execute,
|
|
20
|
-
} from "./src/runtime/execute.js";
|
|
18
|
+
export { default as execute } from "./src/runtime/execute.js";
|
|
21
19
|
export * as expressionFunction from "./src/runtime/expressionFunction.js";
|
|
22
20
|
export { default as expressionObject } from "./src/runtime/expressionObject.js";
|
|
23
21
|
export * from "./src/runtime/handleExtension.js";
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/language",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.12",
|
|
4
4
|
"description": "Web Origami expression language compiler and runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./main.js",
|
|
7
7
|
"types": "./index.ts",
|
|
8
8
|
"devDependencies": {
|
|
9
|
-
"@types/node": "
|
|
9
|
+
"@types/node": "25.3.2",
|
|
10
10
|
"peggy": "5.0.6",
|
|
11
11
|
"typescript": "5.9.3"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@weborigami/async-tree": "0.6.
|
|
14
|
+
"@weborigami/async-tree": "0.6.12",
|
|
15
15
|
"exif-parser": "0.1.12",
|
|
16
16
|
"watcher": "2.3.1",
|
|
17
|
-
"yaml": "2.8.
|
|
17
|
+
"yaml": "2.8.2"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "peggy --allowed-start-rules=\"*\" --format es src/compiler/origami.pegjs --output src/compiler/parse.js",
|
|
@@ -845,8 +845,10 @@ relationalExpression
|
|
|
845
845
|
// We disallow a newline before the relational operator to support a newline
|
|
846
846
|
// as a separator in an object literal that has an object shorthand property
|
|
847
847
|
// with an angle bracket path. Otherwise the opening angle bracket would be
|
|
848
|
-
// interpreted as a relational operator.
|
|
849
|
-
|
|
848
|
+
// interpreted as a relational operator. In shell mode we require a space to
|
|
849
|
+
// avoid ambiguity with an angle bracket literal as an argument in an implicit
|
|
850
|
+
// parentheses call.
|
|
851
|
+
= head:shiftExpression tail:(inlineSpace @relationalOperator whitespaceRequiredForShell @shiftExpression)* {
|
|
850
852
|
return tail.reduce(makeBinaryOperation, head);
|
|
851
853
|
}
|
|
852
854
|
|
|
@@ -1039,9 +1041,9 @@ uriKey
|
|
|
1039
1041
|
|
|
1040
1042
|
// A single character in a URI key
|
|
1041
1043
|
uriKeyChar
|
|
1042
|
-
// Accept anything
|
|
1043
|
-
//
|
|
1044
|
-
= char:[^/,\)\]\}] !&{ return /\s/.test(char); } { return char; }
|
|
1044
|
+
// Accept anything except whitespace (see notes for `whitespace` term),
|
|
1045
|
+
// brackets, comma, or slash
|
|
1046
|
+
= char:[^/,\(\[\{\)\]\}] !&{ return /\s/.test(char); } { return char; }
|
|
1045
1047
|
/ escapedChar
|
|
1046
1048
|
|
|
1047
1049
|
// A slash-separated path of keys: `a/b/c`
|
|
@@ -1071,6 +1073,10 @@ whitespaceOptionalForProgram
|
|
|
1071
1073
|
= programMode __
|
|
1072
1074
|
/ shellMode
|
|
1073
1075
|
|
|
1076
|
+
whitespaceRequiredForShell
|
|
1077
|
+
= shellMode whitespace
|
|
1078
|
+
/ programMode __
|
|
1079
|
+
|
|
1074
1080
|
whitespaceOrParenthesis
|
|
1075
1081
|
= whitespace
|
|
1076
1082
|
/ "("
|
package/src/compiler/parse.js
CHANGED
|
@@ -357,6 +357,7 @@ function peg$parse(input, options) {
|
|
|
357
357
|
whitespace: peg$parsewhitespace,
|
|
358
358
|
whitespaceChar: peg$parsewhitespaceChar,
|
|
359
359
|
whitespaceOptionalForProgram: peg$parsewhitespaceOptionalForProgram,
|
|
360
|
+
whitespaceRequiredForShell: peg$parsewhitespaceRequiredForShell,
|
|
360
361
|
whitespaceOrParenthesis: peg$parsewhitespaceOrParenthesis,
|
|
361
362
|
whitespaceWithNewLine: peg$parsewhitespaceWithNewLine,
|
|
362
363
|
};
|
|
@@ -459,7 +460,7 @@ function peg$parse(input, options) {
|
|
|
459
460
|
const peg$r19 = /^[^\n\r]/;
|
|
460
461
|
const peg$r20 = /^[!+]/;
|
|
461
462
|
const peg$r21 = /^[\/)\]}]/;
|
|
462
|
-
const peg$r22 = /^[^\/,)\]}]/;
|
|
463
|
+
const peg$r22 = /^[^\/,([{)\]}]/;
|
|
463
464
|
const peg$r23 = /^[a-z]/;
|
|
464
465
|
const peg$r24 = /^[a-z0-9+-.]/;
|
|
465
466
|
const peg$r25 = /^[:]/;
|
|
@@ -589,7 +590,7 @@ function peg$parse(input, options) {
|
|
|
589
590
|
const peg$e122 = peg$literalExpectation("await", false);
|
|
590
591
|
const peg$e123 = peg$literalExpectation("typeof", false);
|
|
591
592
|
const peg$e124 = peg$literalExpectation("void", false);
|
|
592
|
-
const peg$e125 = peg$classExpectation(["/", ",", ")", "]", "}"], true, false, false);
|
|
593
|
+
const peg$e125 = peg$classExpectation(["/", ",", "(", "[", "{", ")", "]", "}"], true, false, false);
|
|
593
594
|
const peg$e126 = peg$otherExpectation("slash-separated path");
|
|
594
595
|
const peg$e127 = peg$classExpectation([["a", "z"]], false, false, false);
|
|
595
596
|
const peg$e128 = peg$classExpectation([["a", "z"], ["0", "9"], ["+", "."]], false, false, false);
|
|
@@ -6313,10 +6314,15 @@ function peg$parse(input, options) {
|
|
|
6313
6314
|
if (s4 !== peg$FAILED) {
|
|
6314
6315
|
s5 = peg$parserelationalOperator();
|
|
6315
6316
|
if (s5 !== peg$FAILED) {
|
|
6316
|
-
s6 = peg$
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6317
|
+
s6 = peg$parsewhitespaceRequiredForShell();
|
|
6318
|
+
if (s6 !== peg$FAILED) {
|
|
6319
|
+
s7 = peg$parseshiftExpression();
|
|
6320
|
+
if (s7 !== peg$FAILED) {
|
|
6321
|
+
s3 = [ s5, s7 ];
|
|
6322
|
+
} else {
|
|
6323
|
+
peg$currPos = s3;
|
|
6324
|
+
s3 = peg$FAILED;
|
|
6325
|
+
}
|
|
6320
6326
|
} else {
|
|
6321
6327
|
peg$currPos = s3;
|
|
6322
6328
|
s3 = peg$FAILED;
|
|
@@ -6336,10 +6342,15 @@ function peg$parse(input, options) {
|
|
|
6336
6342
|
if (s4 !== peg$FAILED) {
|
|
6337
6343
|
s5 = peg$parserelationalOperator();
|
|
6338
6344
|
if (s5 !== peg$FAILED) {
|
|
6339
|
-
s6 = peg$
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6345
|
+
s6 = peg$parsewhitespaceRequiredForShell();
|
|
6346
|
+
if (s6 !== peg$FAILED) {
|
|
6347
|
+
s7 = peg$parseshiftExpression();
|
|
6348
|
+
if (s7 !== peg$FAILED) {
|
|
6349
|
+
s3 = [ s5, s7 ];
|
|
6350
|
+
} else {
|
|
6351
|
+
peg$currPos = s3;
|
|
6352
|
+
s3 = peg$FAILED;
|
|
6353
|
+
}
|
|
6343
6354
|
} else {
|
|
6344
6355
|
peg$currPos = s3;
|
|
6345
6356
|
s3 = peg$FAILED;
|
|
@@ -7834,6 +7845,40 @@ function peg$parse(input, options) {
|
|
|
7834
7845
|
return s0;
|
|
7835
7846
|
}
|
|
7836
7847
|
|
|
7848
|
+
function peg$parsewhitespaceRequiredForShell() {
|
|
7849
|
+
let s0, s1, s2;
|
|
7850
|
+
|
|
7851
|
+
s0 = peg$currPos;
|
|
7852
|
+
s1 = peg$parseshellMode();
|
|
7853
|
+
if (s1 !== peg$FAILED) {
|
|
7854
|
+
s2 = peg$parsewhitespace();
|
|
7855
|
+
if (s2 !== peg$FAILED) {
|
|
7856
|
+
s1 = [s1, s2];
|
|
7857
|
+
s0 = s1;
|
|
7858
|
+
} else {
|
|
7859
|
+
peg$currPos = s0;
|
|
7860
|
+
s0 = peg$FAILED;
|
|
7861
|
+
}
|
|
7862
|
+
} else {
|
|
7863
|
+
peg$currPos = s0;
|
|
7864
|
+
s0 = peg$FAILED;
|
|
7865
|
+
}
|
|
7866
|
+
if (s0 === peg$FAILED) {
|
|
7867
|
+
s0 = peg$currPos;
|
|
7868
|
+
s1 = peg$parseprogramMode();
|
|
7869
|
+
if (s1 !== peg$FAILED) {
|
|
7870
|
+
s2 = peg$parse__();
|
|
7871
|
+
s1 = [s1, s2];
|
|
7872
|
+
s0 = s1;
|
|
7873
|
+
} else {
|
|
7874
|
+
peg$currPos = s0;
|
|
7875
|
+
s0 = peg$FAILED;
|
|
7876
|
+
}
|
|
7877
|
+
}
|
|
7878
|
+
|
|
7879
|
+
return s0;
|
|
7880
|
+
}
|
|
7881
|
+
|
|
7837
7882
|
function peg$parsewhitespaceOrParenthesis() {
|
|
7838
7883
|
let s0;
|
|
7839
7884
|
|
|
@@ -8075,6 +8120,7 @@ const peg$allowedStartRules = [
|
|
|
8075
8120
|
"whitespace",
|
|
8076
8121
|
"whitespaceChar",
|
|
8077
8122
|
"whitespaceOptionalForProgram",
|
|
8123
|
+
"whitespaceRequiredForShell",
|
|
8078
8124
|
"whitespaceOrParenthesis",
|
|
8079
8125
|
"whitespaceWithNewLine"
|
|
8080
8126
|
];
|
|
@@ -634,8 +634,7 @@ function makePossibleSpreadCall(target, args, location) {
|
|
|
634
634
|
return [target, ...args];
|
|
635
635
|
}
|
|
636
636
|
|
|
637
|
-
//
|
|
638
|
-
const applyMethod = annotate([ops.property, target, "apply"], location);
|
|
637
|
+
// We'll need to use ops.apply and ops.flat to handle the spreads.
|
|
639
638
|
const wrappedArgs = args.map((arg) => {
|
|
640
639
|
if (arg[0] === markers.spread) {
|
|
641
640
|
return arg[1];
|
|
@@ -643,8 +642,8 @@ function makePossibleSpreadCall(target, args, location) {
|
|
|
643
642
|
return annotate([ops.array, arg], arg.location);
|
|
644
643
|
}
|
|
645
644
|
});
|
|
646
|
-
const
|
|
647
|
-
return [
|
|
645
|
+
const flattened = annotate([ops.flat, ...wrappedArgs], location);
|
|
646
|
+
return annotate([ops.apply, target, flattened], location);
|
|
648
647
|
}
|
|
649
648
|
|
|
650
649
|
/**
|
|
@@ -26,10 +26,15 @@ export default {
|
|
|
26
26
|
* Supports multiple commands, pipelines, redirects, etc.
|
|
27
27
|
*
|
|
28
28
|
* @param {string} scriptText - Shell code (may contain newlines/side effects)
|
|
29
|
-
* @param {
|
|
29
|
+
* @param {import("@weborigami/async-tree").Stringlike} inputText - Text to pipe to the script's stdin
|
|
30
30
|
* @returns {Promise<string>}
|
|
31
31
|
*/
|
|
32
32
|
function runShellScript(scriptText, inputText) {
|
|
33
|
+
if (inputText instanceof Function) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
"A .sh file expects text input but got a function instead. Did you mean to invoke the function?",
|
|
36
|
+
);
|
|
37
|
+
}
|
|
33
38
|
return new Promise((resolve, reject) => {
|
|
34
39
|
// Use sh -c "<scriptText>" so stdin is free for inputText
|
|
35
40
|
const child = spawn("sh", ["-c", scriptText], {
|
|
@@ -49,7 +54,7 @@ function runShellScript(scriptText, inputText) {
|
|
|
49
54
|
if (code !== 0) {
|
|
50
55
|
/** @type {any} */
|
|
51
56
|
const err = new Error(
|
|
52
|
-
`Shell exited with code ${code}${stderr ? `: ${stderr}` : ""}
|
|
57
|
+
`Shell exited with code ${code}${stderr ? `: ${stderr}` : ""}`,
|
|
53
58
|
);
|
|
54
59
|
err.code = code;
|
|
55
60
|
err.stdout = stdout;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
castArraylike,
|
|
2
3
|
getParent,
|
|
3
4
|
isUnpackable,
|
|
4
5
|
symbols,
|
|
@@ -82,8 +83,13 @@ export default {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
if (hasOriTags) {
|
|
85
|
-
//
|
|
86
|
-
|
|
86
|
+
// Invoke any functions and resolve any promises in the deep data.
|
|
87
|
+
const tree = Tree.from(data, { deep: true });
|
|
88
|
+
data = await Tree.mapReduce(
|
|
89
|
+
tree,
|
|
90
|
+
async (value) => (value instanceof Function ? await value() : value),
|
|
91
|
+
(mapped) => castArraylike(mapped),
|
|
92
|
+
);
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
if (data && typeof data === "object" && Object.isExtensible(data)) {
|
package/src/project/jsGlobals.js
CHANGED
|
@@ -128,7 +128,7 @@ const globals = {
|
|
|
128
128
|
encodeURIComponent,
|
|
129
129
|
escape,
|
|
130
130
|
eval,
|
|
131
|
-
|
|
131
|
+
fetch,
|
|
132
132
|
globalThis,
|
|
133
133
|
isFinite,
|
|
134
134
|
isNaN,
|
|
@@ -150,7 +150,6 @@ const globals = {
|
|
|
150
150
|
true: true,
|
|
151
151
|
|
|
152
152
|
// Special cases
|
|
153
|
-
fetch: fetchWrapper,
|
|
154
153
|
import: importWrapper,
|
|
155
154
|
};
|
|
156
155
|
|
|
@@ -160,14 +159,6 @@ Object.defineProperty(globals, "globalThis", {
|
|
|
160
159
|
value: globals,
|
|
161
160
|
});
|
|
162
161
|
|
|
163
|
-
async function fetchWrapper(resource, options) {
|
|
164
|
-
console.warn(
|
|
165
|
-
"Warning: A plain `fetch` reference will eventually call the standard JavaScript fetch() function. For Origami's fetch behavior, update your code to call Origami.fetch().",
|
|
166
|
-
);
|
|
167
|
-
const response = await fetch(resource, options);
|
|
168
|
-
return response.ok ? await response.arrayBuffer() : undefined;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
162
|
/**
|
|
172
163
|
* @typedef {import("@weborigami/async-tree").AsyncMap} AsyncMap
|
|
173
164
|
*
|
package/src/protocols/package.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Tree, keysFromPath } from "@weborigami/async-tree";
|
|
1
|
+
import { Tree, keysFromPath, pathFromKeys } from "@weborigami/async-tree";
|
|
2
2
|
import projectRoot from "../project/projectRoot.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -11,27 +11,36 @@ export default async function packageProtocol(...args) {
|
|
|
11
11
|
const root = await projectRoot(state);
|
|
12
12
|
|
|
13
13
|
// Identify the path to the package root
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const packageRootKeys = ["node_modules"];
|
|
15
|
+
let name = args.shift();
|
|
16
|
+
packageRootKeys.push(name);
|
|
17
17
|
if (name.startsWith("@")) {
|
|
18
18
|
// First key is an npm organization, add next key as name
|
|
19
|
-
|
|
19
|
+
const nameArg = args.shift();
|
|
20
|
+
name += nameArg;
|
|
21
|
+
packageRootKeys.push(nameArg);
|
|
20
22
|
}
|
|
23
|
+
const packageRootPath = pathFromKeys(packageRootKeys);
|
|
21
24
|
|
|
22
25
|
// Get the package root (top level folder of the package)
|
|
23
|
-
|
|
26
|
+
let packageRoot = await Tree.traverse(root, ...packageRootKeys);
|
|
24
27
|
if (!packageRoot) {
|
|
25
|
-
|
|
28
|
+
// Can't find package -- are we *in* the package?
|
|
29
|
+
const packageJson = await Tree.traverse(root, "package.json");
|
|
30
|
+
const packageData = await packageJson?.unpack();
|
|
31
|
+
if (packageData?.name === name) {
|
|
32
|
+
// Yes, we're in the package itself
|
|
33
|
+
packageRoot = root;
|
|
34
|
+
} else {
|
|
35
|
+
throw new Error(`Can't find ${packageRootPath}`);
|
|
36
|
+
}
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
// Identify the main entry point
|
|
29
40
|
const mainPath = await Tree.traverse(packageRoot, "package.json", "main");
|
|
30
41
|
if (mainPath === undefined) {
|
|
31
42
|
throw new Error(
|
|
32
|
-
`${packageRootPath.
|
|
33
|
-
"/",
|
|
34
|
-
)} doesn't contain a package.json with a "main" entry.`,
|
|
43
|
+
`${packageRootPath} doesn't contain a package.json with a "main" entry.`,
|
|
35
44
|
);
|
|
36
45
|
}
|
|
37
46
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import Watcher from "watcher";
|
|
3
4
|
import TreeEvent from "./TreeEvent.js";
|
|
4
5
|
|
|
@@ -17,6 +18,12 @@ export default function WatchFilesMixin(Base) {
|
|
|
17
18
|
onChange(filePath) {
|
|
18
19
|
// Reset cached values.
|
|
19
20
|
this.subfoldersMap = new Map();
|
|
21
|
+
|
|
22
|
+
// Special case: ignore events in .git folder
|
|
23
|
+
if (filePath.includes(`${path.sep}.git${path.sep}`)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
this.dispatchEvent(new TreeEvent("change", { filePath }));
|
|
21
28
|
}
|
|
22
29
|
|
package/src/runtime/errors.js
CHANGED
|
@@ -20,6 +20,11 @@ const origamiSourceSignals = [
|
|
|
20
20
|
* @param {Error} error
|
|
21
21
|
*/
|
|
22
22
|
export async function formatError(error) {
|
|
23
|
+
// We want to display information for the root cause
|
|
24
|
+
while (error.cause instanceof Error) {
|
|
25
|
+
error = error.cause;
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
// Get the original error message
|
|
24
29
|
let originalMessage;
|
|
25
30
|
// If the first line of the stack is just the error message, use that as the message
|
package/src/runtime/execute.js
CHANGED
|
@@ -19,23 +19,13 @@ export default async function execute(code, state = {}) {
|
|
|
19
19
|
return code;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
let evaluated;
|
|
23
|
-
if (code[0]?.unevaluatedArgs) {
|
|
24
|
-
// Don't evaluate instructions, use as is.
|
|
25
|
-
evaluated = code;
|
|
26
|
-
} else {
|
|
27
|
-
// Evaluate each instruction in the code.
|
|
28
|
-
evaluated = await Promise.all(
|
|
29
|
-
code.map((instruction) => execute(instruction, state)),
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
22
|
// Add the code to the runtime state
|
|
34
23
|
/** @type {import("../../index.ts").CodeContext} */
|
|
35
24
|
const context = { state, code };
|
|
36
25
|
|
|
37
|
-
//
|
|
38
|
-
|
|
26
|
+
// Start by evaluating the head of the instruction
|
|
27
|
+
const [head, ...tail] = code;
|
|
28
|
+
let fn = await execute(head, state);
|
|
39
29
|
|
|
40
30
|
if (!fn) {
|
|
41
31
|
// The code wants to invoke something that's couldn't be found in scope.
|
|
@@ -53,9 +43,23 @@ export default async function execute(code, state = {}) {
|
|
|
53
43
|
fn = await fn.unpack();
|
|
54
44
|
}
|
|
55
45
|
|
|
46
|
+
let args;
|
|
47
|
+
if (fn?.unevaluatedArgs) {
|
|
48
|
+
// Don't evaluate instructions, use as is.
|
|
49
|
+
args = tail;
|
|
50
|
+
} else {
|
|
51
|
+
// Evaluate each instruction in the code.
|
|
52
|
+
args = await Promise.all(
|
|
53
|
+
tail.map((instruction) => execute(instruction, state)),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
56
57
|
if (fn.needsState) {
|
|
57
58
|
// The function is an op that wants the runtime state
|
|
58
59
|
args.push(state);
|
|
60
|
+
} else if (fn.needsContext) {
|
|
61
|
+
// The function is an op that wants the code context
|
|
62
|
+
args.push(context);
|
|
59
63
|
} else if (fn.parentAsTarget && state.parent) {
|
|
60
64
|
// The function wants the code's parent as the `this` target
|
|
61
65
|
fn = fn.bind(state.parent);
|
package/src/runtime/ops.js
CHANGED
|
@@ -26,6 +26,28 @@ export function addition(a, b) {
|
|
|
26
26
|
}
|
|
27
27
|
addOpLabel(addition, "«ops.addition»");
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Flatten the arguments and then apply the function.
|
|
31
|
+
* This is used to handle spreads in function calls.
|
|
32
|
+
*/
|
|
33
|
+
export async function apply(fn, args, state) {
|
|
34
|
+
// TODO: This is starting to recapitulate much of execute()
|
|
35
|
+
if (isUnpackable(fn)) {
|
|
36
|
+
fn = await fn.unpack();
|
|
37
|
+
}
|
|
38
|
+
if (fn.needsState) {
|
|
39
|
+
// The function is an op that wants the runtime state
|
|
40
|
+
args.push(state);
|
|
41
|
+
}
|
|
42
|
+
const result =
|
|
43
|
+
fn instanceof Function
|
|
44
|
+
? await fn(...args) // Invoke the function
|
|
45
|
+
: await Tree.traverseOrThrow(fn, ...args); // Traverse the map.
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
addOpLabel(apply, "«ops.apply»");
|
|
49
|
+
apply.needsState = true;
|
|
50
|
+
|
|
29
51
|
/**
|
|
30
52
|
* Construct an array.
|
|
31
53
|
*
|
|
@@ -157,11 +179,14 @@ addOpLabel(exponentiation, "«ops.exponentiation»");
|
|
|
157
179
|
*/
|
|
158
180
|
export async function flat(...args) {
|
|
159
181
|
const arrays = await Promise.all(
|
|
160
|
-
args.map(async (arg) =>
|
|
161
|
-
|
|
182
|
+
args.map(async (arg) => {
|
|
183
|
+
if (isUnpackable(arg)) {
|
|
184
|
+
arg = await arg.unpack();
|
|
185
|
+
}
|
|
186
|
+
return arg instanceof Array || typeof arg !== "object"
|
|
162
187
|
? arg
|
|
163
|
-
: await Tree.values(arg)
|
|
164
|
-
),
|
|
188
|
+
: await Tree.values(arg);
|
|
189
|
+
}),
|
|
165
190
|
);
|
|
166
191
|
|
|
167
192
|
return arrays.flat();
|
|
@@ -569,8 +569,8 @@ describe("Origami parser", () => {
|
|
|
569
569
|
|
|
570
570
|
test("parentheses arguments with spreads", () => {
|
|
571
571
|
assertParse("callExpression", "fn(a, ...b, ...c)", [
|
|
572
|
-
|
|
573
|
-
|
|
572
|
+
ops.apply,
|
|
573
|
+
[markers.traverse, [markers.reference, "fn"]],
|
|
574
574
|
[
|
|
575
575
|
ops.flat,
|
|
576
576
|
[ops.array, [markers.traverse, [markers.reference, "a"]]],
|
|
@@ -1126,12 +1126,8 @@ Body`,
|
|
|
1126
1126
|
"implicitParenthesesCallExpression",
|
|
1127
1127
|
"concat a.json, ...b.json, c.json",
|
|
1128
1128
|
[
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
[markers.traverse, [markers.reference, "concat"]],
|
|
1132
|
-
"apply",
|
|
1133
|
-
],
|
|
1134
|
-
null,
|
|
1129
|
+
ops.apply,
|
|
1130
|
+
[markers.traverse, [markers.reference, "concat"]],
|
|
1135
1131
|
[
|
|
1136
1132
|
ops.flat,
|
|
1137
1133
|
[ops.array, [markers.traverse, [markers.reference, "a.json"]]],
|
package/test/runtime/ops.test.js
CHANGED
|
@@ -20,6 +20,12 @@ describe("ops", () => {
|
|
|
20
20
|
);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
test("ops.apply applies a function to arguments", async () => {
|
|
24
|
+
const code = createCode([ops.apply, ops.addition, [ops.array, 1, 2]]);
|
|
25
|
+
const result = await execute(code);
|
|
26
|
+
assert.strictEqual(result, 3);
|
|
27
|
+
});
|
|
28
|
+
|
|
23
29
|
test("ops.array creates an array", async () => {
|
|
24
30
|
const code = createCode([ops.array, 1, 2, 3]);
|
|
25
31
|
const result = await execute(code);
|