@weborigami/language 0.6.0 → 0.6.2
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 +1 -1
- package/package.json +2 -2
- package/src/compiler/optimize.js +3 -1
- package/src/compiler/origami.pegjs +2 -0
- package/src/compiler/parse.js +113 -91
- package/src/compiler/parserHelpers.js +3 -7
- package/src/handlers/getSource.js +36 -0
- package/src/handlers/ori_handler.js +5 -20
- package/src/handlers/oridocument_handler.js +5 -28
- package/src/handlers/yaml_handler.js +159 -3
- package/src/project/jsGlobals.js +2 -62
- package/src/protocols/constructHref.js +3 -0
- package/src/protocols/explorehttp.js +13 -0
- package/src/protocols/protocolGlobals.js +1 -0
- package/src/protocols/protocols.js +1 -0
- package/src/runtime/errors.js +27 -7
- package/src/runtime/evaluate.js +1 -1
- package/src/runtime/handleExtension.js +0 -5
- package/src/runtime/ops.js +10 -0
- package/test/compiler/parse.test.js +13 -19
- package/test/handlers/yaml_handler.test.js +28 -1
- package/test/project/jsGlobals.test.js +14 -8
- package/test/runtime/handleExtension.test.js +0 -7
- package/test/runtime/ops.test.js +34 -10
- package/test/runtime/jsGlobals.test.js +0 -21
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
extension,
|
|
3
|
-
getParent,
|
|
4
|
-
toString,
|
|
5
|
-
trailingSlash,
|
|
6
|
-
} from "@weborigami/async-tree";
|
|
1
|
+
import { extension, getParent, trailingSlash } from "@weborigami/async-tree";
|
|
7
2
|
import * as compile from "../compiler/compile.js";
|
|
8
3
|
import projectGlobals from "../project/projectGlobals.js";
|
|
4
|
+
import getSource from "./getSource.js";
|
|
9
5
|
|
|
10
6
|
/**
|
|
11
7
|
* An Origami template document: a plain text file that contains Origami
|
|
@@ -17,30 +13,10 @@ export default {
|
|
|
17
13
|
/** @type {import("@weborigami/async-tree").UnpackFunction} */
|
|
18
14
|
async unpack(packed, options = {}) {
|
|
19
15
|
const parent = getParent(packed, options);
|
|
16
|
+
const source = getSource(packed, options);
|
|
20
17
|
|
|
21
|
-
//
|
|
22
|
-
const text = toString(packed);
|
|
23
|
-
|
|
24
|
-
// See if we can construct a URL to use in error messages
|
|
25
|
-
const key = options.key;
|
|
26
|
-
let url;
|
|
27
|
-
if (key && /** @type {any} */ (parent)?.url) {
|
|
28
|
-
let parentHref = /** @type {any} */ (parent).url.href;
|
|
29
|
-
if (!parentHref.endsWith("/")) {
|
|
30
|
-
parentHref += "/";
|
|
31
|
-
}
|
|
32
|
-
url = new URL(key, parentHref);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Compile the text as an Origami template document
|
|
36
|
-
const source = {
|
|
37
|
-
name: key,
|
|
38
|
-
text,
|
|
39
|
-
url,
|
|
40
|
-
};
|
|
41
|
-
|
|
18
|
+
// Compile the source code as an Origami template document
|
|
42
19
|
const globals = options.globals ?? (await projectGlobals());
|
|
43
|
-
|
|
44
20
|
const defineFn = compile.templateDocument(source, {
|
|
45
21
|
front: options.front,
|
|
46
22
|
globals,
|
|
@@ -51,6 +27,7 @@ export default {
|
|
|
51
27
|
// Invoke the definition to get back the template function
|
|
52
28
|
const result = await defineFn();
|
|
53
29
|
|
|
30
|
+
const key = options.key;
|
|
54
31
|
const resultExtension = key ? extension.extname(key) : null;
|
|
55
32
|
if (resultExtension && Object.isExtensible(result)) {
|
|
56
33
|
// Add sidecar function so this template can be used in a map.
|
|
@@ -1,11 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getParent,
|
|
3
|
+
isUnpackable,
|
|
4
|
+
symbols,
|
|
5
|
+
toString,
|
|
6
|
+
Tree,
|
|
7
|
+
} from "@weborigami/async-tree";
|
|
2
8
|
import * as YAMLModule from "yaml";
|
|
9
|
+
import * as compile from "../compiler/compile.js";
|
|
10
|
+
import projectGlobals from "../project/projectGlobals.js";
|
|
11
|
+
import getSource from "./getSource.js";
|
|
3
12
|
|
|
4
13
|
// The "yaml" package doesn't seem to provide a default export that the browser can
|
|
5
14
|
// recognize, so we have to handle two ways to accommodate Node and the browser.
|
|
6
15
|
// @ts-ignore
|
|
7
16
|
const YAML = YAMLModule.default ?? YAMLModule.YAML;
|
|
8
17
|
|
|
18
|
+
// True if we encountered !ori or !ori.call tags while parsing
|
|
19
|
+
let hasOriTags = false;
|
|
20
|
+
|
|
21
|
+
// When processing the !ori tag, the YAML parser will convert our compiler
|
|
22
|
+
// errors into YAML syntax errors. We track the last compiler error so we can
|
|
23
|
+
// re-throw it.
|
|
24
|
+
let lastCompilerError = null;
|
|
25
|
+
|
|
26
|
+
// The source of the last Origami line parsed in a YAML file, used for errors
|
|
27
|
+
let source;
|
|
28
|
+
|
|
9
29
|
/**
|
|
10
30
|
* A YAML file
|
|
11
31
|
*
|
|
@@ -16,18 +36,154 @@ export default {
|
|
|
16
36
|
mediaType: "application/yaml",
|
|
17
37
|
|
|
18
38
|
/** @type {import("@weborigami/async-tree").UnpackFunction} */
|
|
19
|
-
unpack(packed) {
|
|
39
|
+
async unpack(packed, options = {}) {
|
|
20
40
|
const yaml = toString(packed);
|
|
21
41
|
if (!yaml) {
|
|
22
42
|
throw new Error("Tried to parse something as YAML but it wasn't text.");
|
|
23
43
|
}
|
|
24
|
-
const
|
|
44
|
+
const parent = getParent(packed, options);
|
|
45
|
+
const oriCallTag = await oriCallTagForParent(parent, options, yaml);
|
|
46
|
+
const oriTag = await oriTagForParent(parent, options, yaml);
|
|
47
|
+
hasOriTags = false; // Haven't seen any yet
|
|
48
|
+
|
|
49
|
+
let data;
|
|
50
|
+
// YAML parser is sync, but top-level !ori or !ori.call tags will return a
|
|
51
|
+
// promise.
|
|
52
|
+
try {
|
|
53
|
+
// @ts-ignore TypeScript complains customTags isn't valid here but it is.
|
|
54
|
+
data = YAML.parse(yaml, {
|
|
55
|
+
customTags: [oriCallTag, oriTag],
|
|
56
|
+
});
|
|
57
|
+
} catch (/** @type {any} */ error) {
|
|
58
|
+
if (error.name === "SyntaxError") {
|
|
59
|
+
// One of our compiler errors, probably thrown by !ori.call tag
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
const errorText = yaml.slice(error.pos[0], error.pos[1]);
|
|
63
|
+
const isOriError = errorText === "!ori" || errorText === "!ori.call";
|
|
64
|
+
if (isOriError && lastCompilerError) {
|
|
65
|
+
// Error is in an Origami tag, probably throw by !ori tag. Find the
|
|
66
|
+
// position of the Origami source in the YAML text.
|
|
67
|
+
let offset = error.pos[0] + errorText.length;
|
|
68
|
+
while (/\s/.test(yaml[offset])) {
|
|
69
|
+
offset++;
|
|
70
|
+
}
|
|
71
|
+
lastCompilerError.location.source.offset = offset;
|
|
72
|
+
throw lastCompilerError;
|
|
73
|
+
} else {
|
|
74
|
+
// Some other YAML parsing error
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (data instanceof Promise) {
|
|
80
|
+
// Top-level !ori or !ori.call tag returned a promise
|
|
81
|
+
data = await data;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (hasOriTags) {
|
|
85
|
+
// Resolve any promises in the data.
|
|
86
|
+
data = await Tree.plain(data);
|
|
87
|
+
}
|
|
88
|
+
|
|
25
89
|
if (data && typeof data === "object" && Object.isExtensible(data)) {
|
|
26
90
|
Object.defineProperty(data, symbols.deep, {
|
|
27
91
|
enumerable: false,
|
|
28
92
|
value: true,
|
|
29
93
|
});
|
|
30
94
|
}
|
|
95
|
+
|
|
31
96
|
return data;
|
|
32
97
|
},
|
|
33
98
|
};
|
|
99
|
+
|
|
100
|
+
async function oriCallTagForParent(parent, options, yaml) {
|
|
101
|
+
const globals = await projectGlobals();
|
|
102
|
+
return {
|
|
103
|
+
collection: "seq",
|
|
104
|
+
|
|
105
|
+
tag: "!ori.call",
|
|
106
|
+
|
|
107
|
+
resolve(value) {
|
|
108
|
+
hasOriTags = true;
|
|
109
|
+
/** @type {any[]} */
|
|
110
|
+
const args = typeof value?.toJSON === "function" ? value.toJSON() : value;
|
|
111
|
+
|
|
112
|
+
// First arg is Origami source
|
|
113
|
+
const text = args.shift();
|
|
114
|
+
source = getSource(text, options);
|
|
115
|
+
|
|
116
|
+
// Offset the source position to account for its location in YAML text
|
|
117
|
+
source.context = yaml;
|
|
118
|
+
const firstItem = value.items[0];
|
|
119
|
+
source.offset = firstItem.range[0];
|
|
120
|
+
if (
|
|
121
|
+
firstItem.type === "QUOTE_DOUBLE" ||
|
|
122
|
+
firstItem.type === "QUOTE_SINGLE"
|
|
123
|
+
) {
|
|
124
|
+
// Account for opening quote
|
|
125
|
+
source.offset += 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
lastCompilerError = null;
|
|
129
|
+
let codeFn;
|
|
130
|
+
try {
|
|
131
|
+
codeFn = compile.expression(source, {
|
|
132
|
+
globals,
|
|
133
|
+
parent,
|
|
134
|
+
});
|
|
135
|
+
} catch (error) {
|
|
136
|
+
lastCompilerError = error;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Return a promise for the code's evaluation. If we instead define
|
|
141
|
+
// resolve() as async, the catch block in unpack() won't catch Origami
|
|
142
|
+
// parse errors.
|
|
143
|
+
return new Promise(async (resolve) => {
|
|
144
|
+
// Evaluate the code to get a function
|
|
145
|
+
let fn = await codeFn.call(parent);
|
|
146
|
+
|
|
147
|
+
// Call the function with the rest of the args
|
|
148
|
+
if (isUnpackable(fn)) {
|
|
149
|
+
fn = await fn.unpack();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Resolve any promise args
|
|
153
|
+
const resolvedArgs = await Promise.all(args);
|
|
154
|
+
|
|
155
|
+
const result = await fn.call(null, ...resolvedArgs);
|
|
156
|
+
resolve(result);
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Define the !ori tag for YAML parsing. This will run in the context of the
|
|
163
|
+
// supplied parent.
|
|
164
|
+
async function oriTagForParent(parent, options, yaml) {
|
|
165
|
+
const globals = await projectGlobals();
|
|
166
|
+
return {
|
|
167
|
+
resolve(text) {
|
|
168
|
+
hasOriTags = true;
|
|
169
|
+
source = getSource(text, options);
|
|
170
|
+
source.context = yaml;
|
|
171
|
+
|
|
172
|
+
lastCompilerError = null;
|
|
173
|
+
let fn;
|
|
174
|
+
try {
|
|
175
|
+
fn = compile.expression(source, {
|
|
176
|
+
globals,
|
|
177
|
+
parent,
|
|
178
|
+
});
|
|
179
|
+
} catch (error) {
|
|
180
|
+
lastCompilerError = error;
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return fn.call(parent);
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
tag: "!ori",
|
|
188
|
+
};
|
|
189
|
+
}
|
package/src/project/jsGlobals.js
CHANGED
|
@@ -12,7 +12,7 @@ import path from "node:path";
|
|
|
12
12
|
* Fetch API
|
|
13
13
|
* URL API
|
|
14
14
|
*/
|
|
15
|
-
const globals =
|
|
15
|
+
const globals = {
|
|
16
16
|
AbortController,
|
|
17
17
|
AbortSignal,
|
|
18
18
|
AggregateError,
|
|
@@ -152,7 +152,7 @@ const globals = bindStaticMethodsForGlobals({
|
|
|
152
152
|
// Special cases
|
|
153
153
|
fetch: fetchWrapper,
|
|
154
154
|
import: importWrapper,
|
|
155
|
-
}
|
|
155
|
+
};
|
|
156
156
|
|
|
157
157
|
// Give access to our own custom globals as `globalThis`
|
|
158
158
|
Object.defineProperty(globals, "globalThis", {
|
|
@@ -190,64 +190,4 @@ async function importWrapper(modulePath) {
|
|
|
190
190
|
}
|
|
191
191
|
importWrapper.containerAsTarget = true;
|
|
192
192
|
|
|
193
|
-
/**
|
|
194
|
-
* Some JavaScript globals like Promise have static methods like Promise.all
|
|
195
|
-
* verify that the call target is the class. This creates an issue because the
|
|
196
|
-
* Origami evaluate() function calls all functions with the evaluation context
|
|
197
|
-
* -- the tree in which the code is running -- as the call target.
|
|
198
|
-
*
|
|
199
|
-
* This function works around the problem. If the indicated object has no static
|
|
200
|
-
* methods, it's returned as is. If it does have static methods, this returns an
|
|
201
|
-
* extension of the object that overrides the static methods with ones that are
|
|
202
|
-
* bound to the object.
|
|
203
|
-
*/
|
|
204
|
-
function bindStaticMethods(obj) {
|
|
205
|
-
if (typeof obj !== "function" && (typeof obj !== "object" || obj === null)) {
|
|
206
|
-
// Something like `NaN` or `null`
|
|
207
|
-
return obj;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const staticMethodDescriptors = Object.entries(
|
|
211
|
-
Object.getOwnPropertyDescriptors(obj)
|
|
212
|
-
)
|
|
213
|
-
.filter(([key, descriptor]) => descriptor.value instanceof Function)
|
|
214
|
-
.map(([key, descriptor]) => [
|
|
215
|
-
key,
|
|
216
|
-
{
|
|
217
|
-
...descriptor,
|
|
218
|
-
value: descriptor.value.bind(obj),
|
|
219
|
-
},
|
|
220
|
-
]);
|
|
221
|
-
if (staticMethodDescriptors.length === 0) {
|
|
222
|
-
// No static methods
|
|
223
|
-
return obj;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
let extended;
|
|
227
|
-
if (typeof obj === "object" || !obj.prototype) {
|
|
228
|
-
// A regular object or an oddball like Proxy with no prototype
|
|
229
|
-
extended = Object.create(obj);
|
|
230
|
-
} else {
|
|
231
|
-
// A function, possibly a constructor called with or without `new`
|
|
232
|
-
/** @this {any} */
|
|
233
|
-
extended = function (...args) {
|
|
234
|
-
const calledWithNew = this instanceof extended;
|
|
235
|
-
return calledWithNew ? new obj(...args) : obj(...args);
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
Object.defineProperties(
|
|
240
|
-
extended,
|
|
241
|
-
Object.fromEntries(staticMethodDescriptors)
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
return extended;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function bindStaticMethodsForGlobals(objects) {
|
|
248
|
-
const entries = Object.entries(objects);
|
|
249
|
-
const bound = entries.map(([key, value]) => [key, bindStaticMethods(value)]);
|
|
250
|
-
return Object.fromEntries(bound);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
193
|
export default globals;
|
|
@@ -9,6 +9,9 @@ import { pathFromKeys } from "@weborigami/async-tree";
|
|
|
9
9
|
*/
|
|
10
10
|
export default function constructHref(protocol, host, ...keys) {
|
|
11
11
|
const path = pathFromKeys(keys);
|
|
12
|
+
if (host.endsWith("/")) {
|
|
13
|
+
host = host.slice(0, -1);
|
|
14
|
+
}
|
|
12
15
|
let href = [host, path].join("/");
|
|
13
16
|
if (!href.startsWith(protocol)) {
|
|
14
17
|
if (!href.startsWith("//")) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ExplorableSiteMap } from "@weborigami/async-tree";
|
|
2
|
+
import constructSiteTree from "./constructSiteTree.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A site tree with JSON Keys via HTTP.
|
|
6
|
+
*
|
|
7
|
+
*
|
|
8
|
+
* @param {string} host
|
|
9
|
+
* @param {...string} keys
|
|
10
|
+
*/
|
|
11
|
+
export default function explorehttp(host, ...keys) {
|
|
12
|
+
return constructSiteTree("http:", ExplorableSiteMap, host, ...keys);
|
|
13
|
+
}
|
package/src/runtime/errors.js
CHANGED
|
@@ -95,7 +95,9 @@ export function formatError(error) {
|
|
|
95
95
|
error.message === "A null or undefined value can't be traversed"
|
|
96
96
|
) {
|
|
97
97
|
// Provide more meaningful message for TraverseError
|
|
98
|
-
line = `TraverseError: This part of the path is null or undefined: ${
|
|
98
|
+
line = `TraverseError: This part of the path is null or undefined: ${highlightError(
|
|
99
|
+
fragment
|
|
100
|
+
)}`;
|
|
99
101
|
fragmentInMessage = true;
|
|
100
102
|
}
|
|
101
103
|
if (message) {
|
|
@@ -110,7 +112,7 @@ export function formatError(error) {
|
|
|
110
112
|
// Add location
|
|
111
113
|
if (location) {
|
|
112
114
|
if (!fragmentInMessage) {
|
|
113
|
-
message += `\nevaluating: ${fragment}`;
|
|
115
|
+
message += `\nevaluating: ${highlightError(fragment)}`;
|
|
114
116
|
}
|
|
115
117
|
message += lineInfo(location);
|
|
116
118
|
}
|
|
@@ -130,6 +132,11 @@ export async function formatScopeTypos(scope, key) {
|
|
|
130
132
|
return `Maybe you meant ${list}?`;
|
|
131
133
|
}
|
|
132
134
|
|
|
135
|
+
export function highlightError(text) {
|
|
136
|
+
// ANSI escape sequence to highlight text in red
|
|
137
|
+
return `\x1b[31m${text}\x1b[0m`;
|
|
138
|
+
}
|
|
139
|
+
|
|
133
140
|
export function maybeOrigamiSourceCode(text) {
|
|
134
141
|
return origamiSourceSignals.some((signal) => text.includes(signal));
|
|
135
142
|
}
|
|
@@ -137,9 +144,22 @@ export function maybeOrigamiSourceCode(text) {
|
|
|
137
144
|
// Return user-friendly line information for the error location
|
|
138
145
|
function lineInfo(location) {
|
|
139
146
|
let { source, start } = location;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
let
|
|
147
|
+
|
|
148
|
+
let line;
|
|
149
|
+
let column;
|
|
150
|
+
if (source.offset && source.context) {
|
|
151
|
+
// Account for source code that was offset within a larger document
|
|
152
|
+
const offset = source.offset + start.offset;
|
|
153
|
+
// Calculate line and column from offset
|
|
154
|
+
const textUpToOffset = source.context.slice(0, offset);
|
|
155
|
+
const lines = textUpToOffset.split("\n");
|
|
156
|
+
line = lines.length;
|
|
157
|
+
column = lines[lines.length - 1].length + 1;
|
|
158
|
+
} else {
|
|
159
|
+
// Use indicated start location as is
|
|
160
|
+
line = start.line;
|
|
161
|
+
column = start.column;
|
|
162
|
+
}
|
|
143
163
|
|
|
144
164
|
if (typeof source === "object" && source.url) {
|
|
145
165
|
const { url } = source;
|
|
@@ -155,10 +175,10 @@ function lineInfo(location) {
|
|
|
155
175
|
// Not a file: URL, use as is
|
|
156
176
|
fileRef = url.href;
|
|
157
177
|
}
|
|
158
|
-
return `\n at ${fileRef}:${line}:${
|
|
178
|
+
return `\n at ${fileRef}:${line}:${column}`;
|
|
159
179
|
} else if (source.text.includes("\n")) {
|
|
160
180
|
// Don't know the URL, but has multiple lines so add line number
|
|
161
|
-
return `\n at line ${line}, column ${
|
|
181
|
+
return `\n at line ${line}, column ${column}`;
|
|
162
182
|
} else {
|
|
163
183
|
return "";
|
|
164
184
|
}
|
package/src/runtime/evaluate.js
CHANGED
|
@@ -36,7 +36,7 @@ export default async function evaluate(code, state = {}) {
|
|
|
36
36
|
const error = ReferenceError(
|
|
37
37
|
`${codeFragment(code[0].location)} is not defined`
|
|
38
38
|
);
|
|
39
|
-
/** @type {any} */ (error).location = code.location;
|
|
39
|
+
/** @type {any} */ (error).location = code[0].location;
|
|
40
40
|
throw error;
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -41,11 +41,6 @@ export default async function handleExtension(value, key, parent) {
|
|
|
41
41
|
handler = await handler.unpack();
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
if (hasSlash && handler.unpack) {
|
|
45
|
-
// Key like `data.json/` ends in slash -- unpack immediately
|
|
46
|
-
return handler.unpack(value, { key, parent });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
44
|
// If the value is a primitive, box it so we can attach data to it.
|
|
50
45
|
value = box(value);
|
|
51
46
|
|
package/src/runtime/ops.js
CHANGED
|
@@ -181,6 +181,11 @@ export async function homeDirectory(...keys) {
|
|
|
181
181
|
}
|
|
182
182
|
addOpLabel(homeDirectory, "«ops.homeDirectory»");
|
|
183
183
|
|
|
184
|
+
export function inOperator(a, b) {
|
|
185
|
+
return a in b;
|
|
186
|
+
}
|
|
187
|
+
addOpLabel(inOperator, "«ops.inOperator»");
|
|
188
|
+
|
|
184
189
|
/**
|
|
185
190
|
* Given the tree currently be using as the context for the runtime, walk up the
|
|
186
191
|
* parent chain `depth` levels and return that tree.
|
|
@@ -203,6 +208,11 @@ export async function inherited(depth, state) {
|
|
|
203
208
|
addOpLabel(inherited, "«ops.inherited»");
|
|
204
209
|
inherited.needsState = true;
|
|
205
210
|
|
|
211
|
+
export function instanceOf(a, b) {
|
|
212
|
+
return a instanceof b;
|
|
213
|
+
}
|
|
214
|
+
addOpLabel(instanceOf, "«ops.instanceOf»");
|
|
215
|
+
|
|
206
216
|
/**
|
|
207
217
|
* Return a function that will invoke the given code.
|
|
208
218
|
*
|
|
@@ -233,8 +233,8 @@ describe("Origami parser", () => {
|
|
|
233
233
|
|
|
234
234
|
test("with paths", () => {
|
|
235
235
|
assertParse("callExpression", "tree/", [
|
|
236
|
-
|
|
237
|
-
[markers.
|
|
236
|
+
markers.traverse,
|
|
237
|
+
[markers.reference, "tree/"],
|
|
238
238
|
]);
|
|
239
239
|
assertParse("callExpression", "tree/foo/bar", [
|
|
240
240
|
markers.traverse,
|
|
@@ -243,13 +243,10 @@ describe("Origami parser", () => {
|
|
|
243
243
|
[ops.literal, "bar"],
|
|
244
244
|
]);
|
|
245
245
|
assertParse("callExpression", "tree/foo/bar/", [
|
|
246
|
-
|
|
247
|
-
[
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
[ops.literal, "foo/"],
|
|
251
|
-
[ops.literal, "bar/"],
|
|
252
|
-
],
|
|
246
|
+
markers.traverse,
|
|
247
|
+
[markers.reference, "tree/"],
|
|
248
|
+
[ops.literal, "foo/"],
|
|
249
|
+
[ops.literal, "bar/"],
|
|
253
250
|
]);
|
|
254
251
|
// Consecutive slahes in a path are removed
|
|
255
252
|
assertParse("callExpression", "tree//key", [
|
|
@@ -1032,7 +1029,7 @@ Body`,
|
|
|
1032
1029
|
]);
|
|
1033
1030
|
assertParse("objectEntry", "folder/", [
|
|
1034
1031
|
"folder/",
|
|
1035
|
-
[
|
|
1032
|
+
[markers.traverse, [markers.reference, "folder/"]],
|
|
1036
1033
|
]);
|
|
1037
1034
|
assertParse("objectEntry", "path/to/file.txt", [
|
|
1038
1035
|
"file.txt",
|
|
@@ -1202,8 +1199,8 @@ Body`,
|
|
|
1202
1199
|
[markers.reference, "tree"],
|
|
1203
1200
|
]);
|
|
1204
1201
|
assertParse("pathLiteral", "tree/", [
|
|
1205
|
-
|
|
1206
|
-
[markers.
|
|
1202
|
+
markers.traverse,
|
|
1203
|
+
[markers.reference, "tree/"],
|
|
1207
1204
|
]);
|
|
1208
1205
|
assertParse("pathLiteral", "month/12", [
|
|
1209
1206
|
markers.traverse,
|
|
@@ -1211,13 +1208,10 @@ Body`,
|
|
|
1211
1208
|
[ops.literal, "12"],
|
|
1212
1209
|
]);
|
|
1213
1210
|
assertParse("pathLiteral", "a/b/c/", [
|
|
1214
|
-
|
|
1215
|
-
[
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
[ops.literal, "b/"],
|
|
1219
|
-
[ops.literal, "c/"],
|
|
1220
|
-
],
|
|
1211
|
+
markers.traverse,
|
|
1212
|
+
[markers.reference, "a/"],
|
|
1213
|
+
[ops.literal, "b/"],
|
|
1214
|
+
[ops.literal, "c/"],
|
|
1221
1215
|
]);
|
|
1222
1216
|
assertParse("pathLiteral", "~/.cshrc", [
|
|
1223
1217
|
markers.traverse,
|
|
@@ -8,10 +8,37 @@ describe(".yaml handler", () => {
|
|
|
8
8
|
a: 1
|
|
9
9
|
b: 2
|
|
10
10
|
`;
|
|
11
|
-
const data = yaml_handler.unpack(text);
|
|
11
|
+
const data = await yaml_handler.unpack(text);
|
|
12
12
|
assert.deepEqual(data, {
|
|
13
13
|
a: 1,
|
|
14
14
|
b: 2,
|
|
15
15
|
});
|
|
16
16
|
});
|
|
17
|
+
|
|
18
|
+
test("defines !ori tag for Origami expressions", async () => {
|
|
19
|
+
const text = `
|
|
20
|
+
message: Hello
|
|
21
|
+
answer: !ori 1 + 1
|
|
22
|
+
`;
|
|
23
|
+
const data = await yaml_handler.unpack(text);
|
|
24
|
+
assert.deepEqual(data, {
|
|
25
|
+
message: "Hello",
|
|
26
|
+
answer: 2,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("defines !ori.call tag for Origami function invocation", async () => {
|
|
31
|
+
const text = `
|
|
32
|
+
message: Hello
|
|
33
|
+
answer: !ori.call
|
|
34
|
+
- (a, b) => a + b
|
|
35
|
+
- 2
|
|
36
|
+
- 3
|
|
37
|
+
`;
|
|
38
|
+
const data = await yaml_handler.unpack(text);
|
|
39
|
+
assert.deepEqual(data, {
|
|
40
|
+
message: "Hello",
|
|
41
|
+
answer: 5,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
17
44
|
});
|
|
@@ -3,19 +3,25 @@ import { describe, test } from "node:test";
|
|
|
3
3
|
import jsGlobals from "../../src/project/jsGlobals.js";
|
|
4
4
|
|
|
5
5
|
describe("jsGlobals", () => {
|
|
6
|
-
test("can invoke
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
const result = (
|
|
10
|
-
await all(["fruit", "computer", "park"].map((item) => `Apple ${item}`))
|
|
11
|
-
).join(", ");
|
|
12
|
-
assert.equal(result, "Apple fruit, Apple computer, Apple park");
|
|
6
|
+
test("can invoke finicky methods like Promise.all that check their receiver", async () => {
|
|
7
|
+
const value = await jsGlobals.Promise.all([Promise.resolve("hi")]);
|
|
8
|
+
assert.equal(value, "hi");
|
|
13
9
|
});
|
|
14
10
|
|
|
15
|
-
test("can invoke a method on a
|
|
11
|
+
test("can invoke a static method on a global", () => {
|
|
16
12
|
const { Math } = jsGlobals;
|
|
17
13
|
const a = [1, 3, 2];
|
|
18
14
|
const b = Math.max.apply(null, a);
|
|
19
15
|
assert.equal(b, 3);
|
|
20
16
|
});
|
|
17
|
+
|
|
18
|
+
test("can invoke a global constructor", async () => {
|
|
19
|
+
const { Number: fixture } = jsGlobals;
|
|
20
|
+
// Without `new`
|
|
21
|
+
const instance1 = fixture(5);
|
|
22
|
+
assert.equal(instance1, 5);
|
|
23
|
+
// With `new`
|
|
24
|
+
const instance = new fixture();
|
|
25
|
+
assert(instance instanceof Number);
|
|
26
|
+
});
|
|
21
27
|
});
|
|
@@ -15,13 +15,6 @@ describe("handleExtension", () => {
|
|
|
15
15
|
const data = await withHandler.unpack();
|
|
16
16
|
assert.deepEqual(data, { bar: 2 });
|
|
17
17
|
});
|
|
18
|
-
|
|
19
|
-
test("immediately unpacks if key ends in slash", async () => {
|
|
20
|
-
const fixture = createFixture();
|
|
21
|
-
const jsonFile = await fixture.get("bar.json");
|
|
22
|
-
const data = await handleExtension(jsonFile, "bar.json/", fixture);
|
|
23
|
-
assert.deepEqual(data, { bar: 2 });
|
|
24
|
-
});
|
|
25
18
|
});
|
|
26
19
|
|
|
27
20
|
function createFixture() {
|