@weborigami/language 0.0.73 → 0.2.0
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/index.ts +1 -0
- package/main.js +2 -2
- package/package.json +6 -4
- package/src/compiler/compile.js +42 -17
- package/src/compiler/origami.pegjs +248 -182
- package/src/compiler/parse.js +1569 -1231
- package/src/compiler/parserHelpers.js +180 -48
- package/src/runtime/HandleExtensionsTransform.js +1 -1
- package/src/runtime/ImportModulesMixin.js +1 -1
- package/src/runtime/codeFragment.js +2 -2
- package/src/runtime/errors.js +104 -0
- package/src/runtime/evaluate.js +3 -3
- package/src/runtime/expressionObject.js +8 -5
- package/src/runtime/{extensions.js → handlers.js} +6 -24
- package/src/runtime/internal.js +1 -0
- package/src/runtime/ops.js +156 -185
- package/src/runtime/typos.js +71 -0
- package/test/cases/ReadMe.md +1 -0
- package/test/cases/conditionalExpression.yaml +101 -0
- package/test/cases/logicalAndExpression.yaml +146 -0
- package/test/cases/logicalOrExpression.yaml +145 -0
- package/test/cases/nullishCoalescingExpression.yaml +105 -0
- package/test/compiler/compile.test.js +7 -7
- package/test/compiler/parse.test.js +506 -294
- package/test/generated/conditionalExpression.test.js +58 -0
- package/test/generated/logicalAndExpression.test.js +80 -0
- package/test/generated/logicalOrExpression.test.js +78 -0
- package/test/generated/nullishCoalescingExpression.test.js +64 -0
- package/test/generator/generateTests.js +80 -0
- package/test/generator/oriEval.js +15 -0
- package/test/runtime/fixtures/templates/greet.orit +1 -1
- package/test/runtime/{extensions.test.js → handlers.test.js} +2 -2
- package/test/runtime/ops.test.js +129 -26
- package/test/runtime/typos.test.js +21 -0
- package/src/runtime/formatError.js +0 -56
package/src/runtime/ops.js
CHANGED
|
@@ -1,30 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
3
3
|
* @typedef {import("@weborigami/async-tree").PlainObject} PlainObject
|
|
4
|
+
* @typedef {import("@weborigami/async-tree").Treelike} Treelike
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import {
|
|
7
|
-
ExplorableSiteTree,
|
|
8
8
|
ObjectTree,
|
|
9
|
-
SiteTree,
|
|
10
9
|
Tree,
|
|
11
10
|
isUnpackable,
|
|
12
|
-
pathFromKeys,
|
|
13
11
|
scope as scopeFn,
|
|
14
|
-
trailingSlash,
|
|
15
12
|
concat as treeConcat,
|
|
16
13
|
} from "@weborigami/async-tree";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import { builtinReferenceError, scopeReferenceError } from "./errors.js";
|
|
17
16
|
import expressionObject from "./expressionObject.js";
|
|
18
|
-
import { handleExtension } from "./extensions.js";
|
|
19
|
-
import HandleExtensionsTransform from "./HandleExtensionsTransform.js";
|
|
20
17
|
import { evaluate } from "./internal.js";
|
|
21
18
|
import mergeTrees from "./mergeTrees.js";
|
|
22
19
|
import OrigamiFiles from "./OrigamiFiles.js";
|
|
20
|
+
import { codeSymbol } from "./symbols.js";
|
|
23
21
|
import taggedTemplate from "./taggedTemplate.js";
|
|
24
22
|
|
|
25
|
-
// For memoizing lambda functions
|
|
26
|
-
const lambdaFnMap = new Map();
|
|
27
|
-
|
|
28
23
|
function addOpLabel(op, label) {
|
|
29
24
|
Object.defineProperty(op, "toString", {
|
|
30
25
|
value: () => label,
|
|
@@ -44,21 +39,22 @@ export async function array(...items) {
|
|
|
44
39
|
addOpLabel(array, "«ops.array»");
|
|
45
40
|
|
|
46
41
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
42
|
+
* Like ops.scope, but only searches for a builtin at the top of the scope
|
|
43
|
+
* chain.
|
|
49
44
|
*
|
|
50
45
|
* @this {AsyncTree|null}
|
|
51
46
|
*/
|
|
52
|
-
export async function
|
|
53
|
-
if (
|
|
54
|
-
|
|
47
|
+
export async function builtin(key) {
|
|
48
|
+
if (!this) {
|
|
49
|
+
throw new Error("Tried to get the scope of a null or undefined tree.");
|
|
55
50
|
}
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
51
|
+
|
|
52
|
+
const builtins = Tree.root(this);
|
|
53
|
+
const value = await builtins.get(key);
|
|
54
|
+
if (value === undefined) {
|
|
55
|
+
throw await builtinReferenceError(this, builtins, key);
|
|
56
|
+
}
|
|
57
|
+
|
|
62
58
|
return value;
|
|
63
59
|
}
|
|
64
60
|
|
|
@@ -73,104 +69,31 @@ export async function concat(...args) {
|
|
|
73
69
|
}
|
|
74
70
|
addOpLabel(concat, "«ops.concat»");
|
|
75
71
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
* it with `new`.
|
|
79
|
-
*
|
|
80
|
-
* @this {AsyncTree}
|
|
81
|
-
* @param {...any} keys
|
|
82
|
-
*/
|
|
83
|
-
export async function constructor(...keys) {
|
|
84
|
-
const tree = this;
|
|
85
|
-
const scope = scopeFn(tree);
|
|
86
|
-
let constructor = await Tree.traverseOrThrow(scope, ...keys);
|
|
87
|
-
if (isUnpackable(constructor)) {
|
|
88
|
-
constructor = await constructor.unpack();
|
|
89
|
-
}
|
|
90
|
-
// Origami may pass `undefined` as the first argument to the constructor. We
|
|
91
|
-
// don't pass that along, because constructors like `Date` don't like it.
|
|
92
|
-
return (...args) =>
|
|
93
|
-
args.length === 1 && args[0] === undefined
|
|
94
|
-
? new constructor()
|
|
95
|
-
: new constructor(...args);
|
|
96
|
-
}
|
|
97
|
-
addOpLabel(constructor, "«ops.constructor»");
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Given a protocol, a host, and a list of keys, construct an href.
|
|
101
|
-
*
|
|
102
|
-
* @param {string} protocol
|
|
103
|
-
* @param {string} host
|
|
104
|
-
* @param {string[]} keys
|
|
105
|
-
*/
|
|
106
|
-
function constructHref(protocol, host, ...keys) {
|
|
107
|
-
const path = pathFromKeys(keys);
|
|
108
|
-
let href = [host, path].join("/");
|
|
109
|
-
if (!href.startsWith(protocol)) {
|
|
110
|
-
if (!href.startsWith("//")) {
|
|
111
|
-
href = `//${href}`;
|
|
112
|
-
}
|
|
113
|
-
href = `${protocol}${href}`;
|
|
114
|
-
}
|
|
115
|
-
return href;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Given a protocol, a host, and a list of keys, construct an href.
|
|
120
|
-
*
|
|
121
|
-
* @param {string} protocol
|
|
122
|
-
* @param {import("../../index.ts").Constructor<AsyncTree>} treeClass
|
|
123
|
-
* @param {AsyncTree|null} parent
|
|
124
|
-
* @param {string} host
|
|
125
|
-
* @param {string[]} keys
|
|
126
|
-
*/
|
|
127
|
-
async function constructSiteTree(protocol, treeClass, parent, host, ...keys) {
|
|
128
|
-
// If the last key doesn't end in a slash, remove it for now.
|
|
129
|
-
let lastKey;
|
|
130
|
-
if (keys.length > 0 && keys.at(-1) && !trailingSlash.has(keys.at(-1))) {
|
|
131
|
-
lastKey = keys.pop();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const href = constructHref(protocol, host, ...keys);
|
|
135
|
-
let result = new (HandleExtensionsTransform(treeClass))(href);
|
|
136
|
-
result.parent = parent;
|
|
137
|
-
|
|
138
|
-
return lastKey ? result.get(lastKey) : result;
|
|
72
|
+
export async function conditional(condition, truthy, falsy) {
|
|
73
|
+
return condition ? truthy() : falsy();
|
|
139
74
|
}
|
|
140
75
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
*
|
|
144
|
-
* @this {AsyncTree|null}
|
|
145
|
-
* @param {string} host
|
|
146
|
-
* @param {...string} keys
|
|
147
|
-
*/
|
|
148
|
-
export function explorableSite(host, ...keys) {
|
|
149
|
-
return constructSiteTree("https:", ExplorableSiteTree, this, host, ...keys);
|
|
76
|
+
export async function equal(a, b) {
|
|
77
|
+
return a == b;
|
|
150
78
|
}
|
|
151
|
-
addOpLabel(explorableSite, "«ops.explorableSite»");
|
|
152
79
|
|
|
153
80
|
/**
|
|
154
|
-
*
|
|
81
|
+
* Look up the given key as an external reference and cache the value for future
|
|
82
|
+
* requests.
|
|
155
83
|
*
|
|
156
84
|
* @this {AsyncTree|null}
|
|
157
|
-
* @param {string} href
|
|
158
85
|
*/
|
|
159
|
-
async function
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
return undefined;
|
|
163
|
-
}
|
|
164
|
-
let buffer = await response.arrayBuffer();
|
|
165
|
-
|
|
166
|
-
// Attach any loader defined for the file type.
|
|
167
|
-
const url = new URL(href);
|
|
168
|
-
const filename = url.pathname.split("/").pop();
|
|
169
|
-
if (this && filename) {
|
|
170
|
-
buffer = await handleExtension(this, buffer, filename);
|
|
86
|
+
export async function external(key, cache) {
|
|
87
|
+
if (key in cache) {
|
|
88
|
+
return cache[key];
|
|
171
89
|
}
|
|
172
|
-
|
|
173
|
-
|
|
90
|
+
// First save a promise for the value
|
|
91
|
+
const promise = scope.call(this, key);
|
|
92
|
+
cache[key] = promise;
|
|
93
|
+
const value = await promise;
|
|
94
|
+
// Now update with the actual value
|
|
95
|
+
cache[key] = value;
|
|
96
|
+
return value;
|
|
174
97
|
}
|
|
175
98
|
|
|
176
99
|
/**
|
|
@@ -180,47 +103,16 @@ async function fetchResponse(href) {
|
|
|
180
103
|
export const getter = new String("«ops.getter»");
|
|
181
104
|
|
|
182
105
|
/**
|
|
183
|
-
*
|
|
106
|
+
* Files tree for the user's home directory.
|
|
184
107
|
*
|
|
185
108
|
* @this {AsyncTree|null}
|
|
186
109
|
*/
|
|
187
|
-
export async function
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// (e.g., Origami expressions loaded from .ori files) will have access to
|
|
192
|
-
// things like the built-in functions.
|
|
193
|
-
root.parent = this;
|
|
194
|
-
|
|
195
|
-
return root;
|
|
110
|
+
export async function homeDirectory() {
|
|
111
|
+
const tree = new OrigamiFiles(os.homedir());
|
|
112
|
+
tree.parent = this ? Tree.root(this) : null;
|
|
113
|
+
return tree;
|
|
196
114
|
}
|
|
197
115
|
|
|
198
|
-
/**
|
|
199
|
-
* Retrieve a web resource via HTTP.
|
|
200
|
-
*
|
|
201
|
-
* @this {AsyncTree|null}
|
|
202
|
-
* @param {string} host
|
|
203
|
-
* @param {...string} keys
|
|
204
|
-
*/
|
|
205
|
-
export async function http(host, ...keys) {
|
|
206
|
-
const href = constructHref("http:", host, ...keys);
|
|
207
|
-
return fetchResponse.call(this, href);
|
|
208
|
-
}
|
|
209
|
-
addOpLabel(http, "«ops.http»");
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Retrieve a web resource via HTTPS.
|
|
213
|
-
*
|
|
214
|
-
* @this {AsyncTree|null}
|
|
215
|
-
* @param {string} host
|
|
216
|
-
* @param {...string} keys
|
|
217
|
-
*/
|
|
218
|
-
export function https(host, ...keys) {
|
|
219
|
-
const href = constructHref("https:", host, ...keys);
|
|
220
|
-
return fetchResponse.call(this, href);
|
|
221
|
-
}
|
|
222
|
-
addOpLabel(https, "«ops.https»");
|
|
223
|
-
|
|
224
116
|
/**
|
|
225
117
|
* Search the parent's scope -- i.e., exclude the current tree -- for the given
|
|
226
118
|
* key.
|
|
@@ -245,29 +137,37 @@ addOpLabel(inherited, "«ops.inherited»");
|
|
|
245
137
|
* @param {string[]} parameters
|
|
246
138
|
* @param {Code} code
|
|
247
139
|
*/
|
|
248
|
-
|
|
249
140
|
export function lambda(parameters, code) {
|
|
250
|
-
|
|
251
|
-
return lambdaFnMap.get(code);
|
|
252
|
-
}
|
|
141
|
+
const context = this;
|
|
253
142
|
|
|
254
|
-
/** @this {
|
|
143
|
+
/** @this {Treelike|null} */
|
|
255
144
|
async function invoke(...args) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
145
|
+
let target;
|
|
146
|
+
if (parameters.length === 0) {
|
|
147
|
+
// No parameters
|
|
148
|
+
target = context;
|
|
149
|
+
} else {
|
|
150
|
+
// Add arguments to scope.
|
|
151
|
+
const ambients = {};
|
|
152
|
+
for (const parameter of parameters) {
|
|
153
|
+
ambients[parameter] = args.shift();
|
|
154
|
+
}
|
|
155
|
+
Object.defineProperty(ambients, codeSymbol, {
|
|
156
|
+
value: code,
|
|
157
|
+
enumerable: false,
|
|
158
|
+
});
|
|
159
|
+
const ambientTree = new ObjectTree(ambients);
|
|
160
|
+
ambientTree.parent = context;
|
|
161
|
+
target = ambientTree;
|
|
260
162
|
}
|
|
261
|
-
const ambientTree = new ObjectTree(ambients);
|
|
262
|
-
ambientTree.parent = this;
|
|
263
163
|
|
|
264
|
-
let result = await evaluate.call(
|
|
164
|
+
let result = await evaluate.call(target, code);
|
|
265
165
|
|
|
266
166
|
// Bind a function result to the ambients so that it has access to the
|
|
267
167
|
// parameter values -- i.e., like a closure.
|
|
268
168
|
if (result instanceof Function) {
|
|
269
169
|
const resultCode = result.code;
|
|
270
|
-
result = result.bind(
|
|
170
|
+
result = result.bind(target);
|
|
271
171
|
if (code) {
|
|
272
172
|
// Copy over Origami code
|
|
273
173
|
result.code = resultCode;
|
|
@@ -286,7 +186,6 @@ export function lambda(parameters, code) {
|
|
|
286
186
|
});
|
|
287
187
|
|
|
288
188
|
invoke.code = code;
|
|
289
|
-
lambdaFnMap.set(code, invoke);
|
|
290
189
|
return invoke;
|
|
291
190
|
}
|
|
292
191
|
addOpLabel(lambda, "«ops.lambda");
|
|
@@ -299,6 +198,53 @@ export async function literal(value) {
|
|
|
299
198
|
}
|
|
300
199
|
addOpLabel(literal, "«ops.literal»");
|
|
301
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Logical AND operator
|
|
203
|
+
*/
|
|
204
|
+
export async function logicalAnd(head, ...tail) {
|
|
205
|
+
if (!head) {
|
|
206
|
+
return head;
|
|
207
|
+
}
|
|
208
|
+
// Evaluate the tail arguments in order, short-circuiting if any are falsy.
|
|
209
|
+
let lastValue;
|
|
210
|
+
for (const arg of tail) {
|
|
211
|
+
lastValue = arg instanceof Function ? await arg() : arg;
|
|
212
|
+
if (!lastValue) {
|
|
213
|
+
return lastValue;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Return the last value (not `true`)
|
|
218
|
+
return lastValue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Logical NOT operator
|
|
223
|
+
*/
|
|
224
|
+
export async function logicalNot(value) {
|
|
225
|
+
return !value;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Logical OR operator
|
|
230
|
+
*/
|
|
231
|
+
export async function logicalOr(head, ...tail) {
|
|
232
|
+
if (head) {
|
|
233
|
+
return head;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Evaluate the tail arguments in order, short-circuiting if any are truthy.
|
|
237
|
+
let lastValue;
|
|
238
|
+
for (const arg of tail) {
|
|
239
|
+
lastValue = arg instanceof Function ? await arg() : arg;
|
|
240
|
+
if (lastValue) {
|
|
241
|
+
return lastValue;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return lastValue;
|
|
246
|
+
}
|
|
247
|
+
|
|
302
248
|
/**
|
|
303
249
|
* Merge the given trees. If they are all plain objects, return a plain object.
|
|
304
250
|
*
|
|
@@ -323,6 +269,47 @@ export async function object(...entries) {
|
|
|
323
269
|
}
|
|
324
270
|
addOpLabel(object, "«ops.object»");
|
|
325
271
|
|
|
272
|
+
export async function notEqual(a, b) {
|
|
273
|
+
return a != b;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function notStrictEqual(a, b) {
|
|
277
|
+
return a !== b;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Nullish coalescing operator
|
|
282
|
+
*/
|
|
283
|
+
export async function nullishCoalescing(head, ...tail) {
|
|
284
|
+
if (head != null) {
|
|
285
|
+
return head;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let lastValue;
|
|
289
|
+
for (const arg of tail) {
|
|
290
|
+
lastValue = arg instanceof Function ? await arg() : arg;
|
|
291
|
+
if (lastValue != null) {
|
|
292
|
+
return lastValue;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return lastValue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Files tree for the filesystem root.
|
|
301
|
+
*
|
|
302
|
+
* @this {AsyncTree|null}
|
|
303
|
+
*/
|
|
304
|
+
export async function rootDirectory(key) {
|
|
305
|
+
let tree = new OrigamiFiles("/");
|
|
306
|
+
// We set the builtins as the parent because logically the filesystem root is
|
|
307
|
+
// outside the project. This ignores the edge case where the project itself is
|
|
308
|
+
// the root of the filesystem and has a config file.
|
|
309
|
+
tree.parent = this ? Tree.root(this) : null;
|
|
310
|
+
return key ? tree.get(key) : tree;
|
|
311
|
+
}
|
|
312
|
+
|
|
326
313
|
/**
|
|
327
314
|
* Look up the given key in the scope for the current tree.
|
|
328
315
|
*
|
|
@@ -333,7 +320,11 @@ export async function scope(key) {
|
|
|
333
320
|
throw new Error("Tried to get the scope of a null or undefined tree.");
|
|
334
321
|
}
|
|
335
322
|
const scope = scopeFn(this);
|
|
336
|
-
|
|
323
|
+
const value = await scope.get(key);
|
|
324
|
+
if (value === undefined && key !== "undefined") {
|
|
325
|
+
throw await scopeReferenceError(scope, key);
|
|
326
|
+
}
|
|
327
|
+
return value;
|
|
337
328
|
}
|
|
338
329
|
addOpLabel(scope, "«ops.scope»");
|
|
339
330
|
|
|
@@ -343,11 +334,15 @@ addOpLabel(scope, "«ops.scope»");
|
|
|
343
334
|
*/
|
|
344
335
|
export function spread(...args) {
|
|
345
336
|
throw new Error(
|
|
346
|
-
"
|
|
337
|
+
"Internal error: a spread operation wasn't compiled correctly."
|
|
347
338
|
);
|
|
348
339
|
}
|
|
349
340
|
addOpLabel(spread, "«ops.spread»");
|
|
350
341
|
|
|
342
|
+
export async function strictEqual(a, b) {
|
|
343
|
+
return a === b;
|
|
344
|
+
}
|
|
345
|
+
|
|
351
346
|
/**
|
|
352
347
|
* Apply the default tagged template function.
|
|
353
348
|
*/
|
|
@@ -361,30 +356,6 @@ addOpLabel(template, "«ops.template»");
|
|
|
361
356
|
*/
|
|
362
357
|
export const traverse = Tree.traverseOrThrow;
|
|
363
358
|
|
|
364
|
-
/**
|
|
365
|
-
* A website tree via HTTP.
|
|
366
|
-
*
|
|
367
|
-
* @this {AsyncTree|null}
|
|
368
|
-
* @param {string} host
|
|
369
|
-
* @param {...string} keys
|
|
370
|
-
*/
|
|
371
|
-
export function treeHttp(host, ...keys) {
|
|
372
|
-
return constructSiteTree("http:", SiteTree, this, host, ...keys);
|
|
373
|
-
}
|
|
374
|
-
addOpLabel(treeHttp, "«ops.treeHttp»");
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* A website tree via HTTPS.
|
|
378
|
-
*
|
|
379
|
-
* @this {AsyncTree|null}
|
|
380
|
-
* @param {string} host
|
|
381
|
-
* @param {...string} keys
|
|
382
|
-
*/
|
|
383
|
-
export function treeHttps(host, ...keys) {
|
|
384
|
-
return constructSiteTree("https:", SiteTree, this, host, ...keys);
|
|
385
|
-
}
|
|
386
|
-
addOpLabel(treeHttps, "«ops.treeHttps»");
|
|
387
|
-
|
|
388
359
|
/**
|
|
389
360
|
* If the value is packed but has an unpack method, call it and return that as
|
|
390
361
|
* the result; otherwise, return the value as is.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true if the two strings have a Damerau-Levenshtein distance of 1.
|
|
3
|
+
* This will be true if the strings differ by a single insertion, deletion,
|
|
4
|
+
* substitution, or transposition.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} s1
|
|
7
|
+
* @param {string} s2
|
|
8
|
+
*/
|
|
9
|
+
export function isTypo(s1, s2) {
|
|
10
|
+
const length1 = s1.length;
|
|
11
|
+
const length2 = s2.length;
|
|
12
|
+
|
|
13
|
+
// If the strings are identical, distance is 0
|
|
14
|
+
if (s1 === s2) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// If length difference is more than 1, distance can't be 1
|
|
19
|
+
if (Math.abs(length1 - length2) > 1) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (length1 === length2) {
|
|
24
|
+
// Check for one substitution
|
|
25
|
+
let differences = 0;
|
|
26
|
+
for (let i = 0; i < length1; i++) {
|
|
27
|
+
if (s1[i] !== s2[i]) {
|
|
28
|
+
differences++;
|
|
29
|
+
if (differences > 1) {
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (differences === 1) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check for one transposition
|
|
39
|
+
for (let i = 0; i < length1 - 1; i++) {
|
|
40
|
+
if (s1[i] !== s2[i]) {
|
|
41
|
+
// Check if swapping s1[i] and s1[i+1] matches s2
|
|
42
|
+
if (s1[i] === s2[i + 1] && s1[i + 1] === s2[i]) {
|
|
43
|
+
return s1.slice(i + 2) === s2.slice(i + 2);
|
|
44
|
+
} else {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check for one insertion/deletion
|
|
52
|
+
const longer = length1 > length2 ? s1 : s2;
|
|
53
|
+
const shorter = length1 > length2 ? s2 : s1;
|
|
54
|
+
for (let i = 0; i < shorter.length; i++) {
|
|
55
|
+
if (shorter[i] !== longer[i]) {
|
|
56
|
+
// If we skip this character, do the rest match?
|
|
57
|
+
return shorter.slice(i) === longer.slice(i + 1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return shorter === longer.slice(0, shorter.length);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Return any strings that could be a typo of s
|
|
65
|
+
*
|
|
66
|
+
* @param {string} s
|
|
67
|
+
* @param {string[]} strings
|
|
68
|
+
*/
|
|
69
|
+
export function typos(s, strings) {
|
|
70
|
+
return strings.filter((str) => isTypo(s, str));
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This folder defines expression tests in YAML files so that we can programmatically test the evaluation of the expressions in both JavaScript and Origami.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Conditional (ternary) expression tests
|
|
2
|
+
|
|
3
|
+
- source: "true ? 42 : 0"
|
|
4
|
+
expected: 42
|
|
5
|
+
description: "Condition is true, evaluates and returns the first operand"
|
|
6
|
+
|
|
7
|
+
- source: "false ? 42 : 0"
|
|
8
|
+
expected: 0
|
|
9
|
+
description: "Condition is false, evaluates and returns the second operand"
|
|
10
|
+
|
|
11
|
+
- source: "1 ? 'yes' : 'no'"
|
|
12
|
+
expected: "yes"
|
|
13
|
+
description: "Truthy condition with string operands"
|
|
14
|
+
|
|
15
|
+
- source: "0 ? 'yes' : 'no'"
|
|
16
|
+
expected: "no"
|
|
17
|
+
description: "Falsy condition with string operands"
|
|
18
|
+
|
|
19
|
+
- source: "'non-empty' ? 1 : 2"
|
|
20
|
+
expected: 1
|
|
21
|
+
description: "Truthy string condition with numeric operands"
|
|
22
|
+
|
|
23
|
+
- source: "'' ? 1 : 2"
|
|
24
|
+
expected: 2
|
|
25
|
+
description: "Falsy string condition with numeric operands"
|
|
26
|
+
|
|
27
|
+
- source: "null ? 'a' : 'b'"
|
|
28
|
+
expected: "b"
|
|
29
|
+
description: "Falsy null condition"
|
|
30
|
+
|
|
31
|
+
- source: "undefined ? 'a' : 'b'"
|
|
32
|
+
expected: "b"
|
|
33
|
+
description: "Falsy undefined condition"
|
|
34
|
+
|
|
35
|
+
- source: "NaN ? 'a' : 'b'"
|
|
36
|
+
expected: "b"
|
|
37
|
+
description: "Falsy NaN condition"
|
|
38
|
+
|
|
39
|
+
- source: "42 ? true : false"
|
|
40
|
+
expected: true
|
|
41
|
+
description: "Truthy numeric condition with boolean operands"
|
|
42
|
+
|
|
43
|
+
- source: "0 ? true : false"
|
|
44
|
+
expected: false
|
|
45
|
+
description: "Falsy numeric condition with boolean operands"
|
|
46
|
+
|
|
47
|
+
- source: "[] ? 'array' : 'no array'"
|
|
48
|
+
expected: "array"
|
|
49
|
+
description: "Truthy array condition"
|
|
50
|
+
|
|
51
|
+
- source: "{} ? 'object' : 'no object'"
|
|
52
|
+
expected: "object"
|
|
53
|
+
description: "Truthy object condition"
|
|
54
|
+
|
|
55
|
+
- source: "false ? null : undefined"
|
|
56
|
+
expected: __undefined__
|
|
57
|
+
description: "Condition is false, returns undefined"
|
|
58
|
+
|
|
59
|
+
- source: "null ? null : null"
|
|
60
|
+
expected: __null__
|
|
61
|
+
description: "Condition is falsy, returns null"
|
|
62
|
+
|
|
63
|
+
- source: "true ? NaN : 42"
|
|
64
|
+
expected: __NaN__
|
|
65
|
+
description: "Condition is true, evaluates and returns NaN"
|
|
66
|
+
|
|
67
|
+
- source: "(true ? 1 : 2) ? 3 : 4"
|
|
68
|
+
expected: 3
|
|
69
|
+
description: "Nested ternary where first expression evaluates to 1, which is truthy"
|
|
70
|
+
|
|
71
|
+
- source: "(false ? 1 : 2) ? 3 : 4"
|
|
72
|
+
expected: 3
|
|
73
|
+
description: "Nested ternary where first expression evaluates to 2, which is truthy"
|
|
74
|
+
|
|
75
|
+
- source: "(false ? 1 : 0) ? 3 : 4"
|
|
76
|
+
expected: 4
|
|
77
|
+
description: "Nested ternary where first expression evaluates to 0, which is falsy"
|
|
78
|
+
|
|
79
|
+
- source: "true ? (false ? 10 : 20) : 30"
|
|
80
|
+
expected: 20
|
|
81
|
+
description: "Nested ternary in the true branch of outer ternary"
|
|
82
|
+
|
|
83
|
+
- source: "false ? (false ? 10 : 20) : 30"
|
|
84
|
+
expected: 30
|
|
85
|
+
description: "Nested ternary in the false branch of outer ternary"
|
|
86
|
+
|
|
87
|
+
# - source: "'truthy' ? 1 + 2 : 3 + 4"
|
|
88
|
+
# expected: 3
|
|
89
|
+
# description: "Evaluates and returns the true branch with an arithmetic expression"
|
|
90
|
+
|
|
91
|
+
# - source: "'' ? 1 + 2 : 3 + 4"
|
|
92
|
+
# expected: 7
|
|
93
|
+
# description: "Evaluates and returns the false branch with an arithmetic expression"
|
|
94
|
+
|
|
95
|
+
- source: "undefined ? undefined : null"
|
|
96
|
+
expected: __null__
|
|
97
|
+
description: "Condition is falsy, returns null"
|
|
98
|
+
|
|
99
|
+
- source: "null ? undefined : undefined"
|
|
100
|
+
expected: __undefined__
|
|
101
|
+
description: "Condition is falsy, returns undefined"
|