@weborigami/language 0.2.6 → 0.2.7
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/package.json +3 -3
- package/src/compiler/parserHelpers.js +10 -14
- package/src/runtime/evaluate.js +7 -1
- package/src/runtime/expressionObject.js +12 -4
- package/src/runtime/mergeTrees.js +2 -0
- package/src/runtime/ops.js +46 -14
- package/src/runtime/typos.js +6 -0
- package/test/compiler/parse.test.js +5 -0
- package/test/runtime/ops.test.js +39 -0
- package/test/runtime/typos.test.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/language",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Web Origami expression language compiler and runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./main.js",
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"yaml": "2.6.1"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@weborigami/async-tree": "0.2.
|
|
16
|
-
"@weborigami/types": "0.2.
|
|
15
|
+
"@weborigami/async-tree": "0.2.7",
|
|
16
|
+
"@weborigami/types": "0.2.7",
|
|
17
17
|
"watcher": "2.3.1"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
@@ -235,12 +235,17 @@ export function makeObject(entries, op) {
|
|
|
235
235
|
|
|
236
236
|
for (let [key, value] of entries) {
|
|
237
237
|
if (key === ops.spread) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
238
|
+
if (value[0] === ops.object) {
|
|
239
|
+
// Spread of an object; fold into current object
|
|
240
|
+
currentEntries.push(...value.slice(1));
|
|
241
|
+
} else {
|
|
242
|
+
// Spread of a tree; accumulate
|
|
243
|
+
if (currentEntries.length > 0) {
|
|
244
|
+
spreads.push([op, ...currentEntries]);
|
|
245
|
+
currentEntries = [];
|
|
246
|
+
}
|
|
247
|
+
spreads.push(value);
|
|
242
248
|
}
|
|
243
|
-
spreads.push(value);
|
|
244
249
|
continue;
|
|
245
250
|
}
|
|
246
251
|
|
|
@@ -253,15 +258,6 @@ export function makeObject(entries, op) {
|
|
|
253
258
|
// Optimize a getter for a primitive value to a regular property
|
|
254
259
|
value = value[1];
|
|
255
260
|
}
|
|
256
|
-
// else if (
|
|
257
|
-
// value[0] === ops.object ||
|
|
258
|
-
// (value[0] === ops.getter &&
|
|
259
|
-
// value[1] instanceof Array &&
|
|
260
|
-
// (value[1][0] === ops.object || value[1][0] === ops.merge))
|
|
261
|
-
// ) {
|
|
262
|
-
// // Add a trailing slash to key to indicate value is a subtree
|
|
263
|
-
// key = trailingSlash.add(key);
|
|
264
|
-
// }
|
|
265
261
|
}
|
|
266
262
|
|
|
267
263
|
currentEntries.push([key, value]);
|
package/src/runtime/evaluate.js
CHANGED
|
@@ -20,7 +20,13 @@ export default async function evaluate(code) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
let evaluated;
|
|
23
|
-
const unevaluatedFns = [
|
|
23
|
+
const unevaluatedFns = [
|
|
24
|
+
ops.external,
|
|
25
|
+
ops.lambda,
|
|
26
|
+
ops.merge,
|
|
27
|
+
ops.object,
|
|
28
|
+
ops.literal,
|
|
29
|
+
];
|
|
24
30
|
if (unevaluatedFns.includes(code[0])) {
|
|
25
31
|
// Don't evaluate instructions, use as is.
|
|
26
32
|
evaluated = code;
|
|
@@ -34,11 +34,13 @@ export default async function expressionObject(entries, parent) {
|
|
|
34
34
|
|
|
35
35
|
let tree;
|
|
36
36
|
const eagerProperties = [];
|
|
37
|
+
const propertyIsEnumerable = {};
|
|
37
38
|
for (let [key, value] of entries) {
|
|
38
39
|
// Determine if we need to define a getter or a regular property. If the key
|
|
39
40
|
// has an extension, we need to define a getter. If the value is code (an
|
|
40
41
|
// array), we need to define a getter -- but if that code takes the form
|
|
41
|
-
// [ops.getter, <primitive>], we can define a
|
|
42
|
+
// [ops.getter, <primitive>] or [ops.literal, <value>], we can define a
|
|
43
|
+
// regular property.
|
|
42
44
|
let defineProperty;
|
|
43
45
|
const extname = extension.extname(key);
|
|
44
46
|
if (extname) {
|
|
@@ -48,6 +50,9 @@ export default async function expressionObject(entries, parent) {
|
|
|
48
50
|
} else if (value[0] === ops.getter && !(value[1] instanceof Array)) {
|
|
49
51
|
defineProperty = true;
|
|
50
52
|
value = value[1];
|
|
53
|
+
} else if (value[0] === ops.literal) {
|
|
54
|
+
defineProperty = true;
|
|
55
|
+
value = value[1];
|
|
51
56
|
} else {
|
|
52
57
|
defineProperty = false;
|
|
53
58
|
}
|
|
@@ -58,6 +63,7 @@ export default async function expressionObject(entries, parent) {
|
|
|
58
63
|
key = key.slice(1, -1);
|
|
59
64
|
enumerable = false;
|
|
60
65
|
}
|
|
66
|
+
propertyIsEnumerable[key] = enumerable;
|
|
61
67
|
|
|
62
68
|
if (defineProperty) {
|
|
63
69
|
// Define simple property
|
|
@@ -105,7 +111,7 @@ export default async function expressionObject(entries, parent) {
|
|
|
105
111
|
Object.defineProperty(object, symbols.keys, {
|
|
106
112
|
configurable: true,
|
|
107
113
|
enumerable: false,
|
|
108
|
-
value: () => keys(object, eagerProperties, entries),
|
|
114
|
+
value: () => keys(object, eagerProperties, propertyIsEnumerable, entries),
|
|
109
115
|
writable: true,
|
|
110
116
|
});
|
|
111
117
|
|
|
@@ -158,6 +164,8 @@ function entryKey(object, eagerProperties, entry) {
|
|
|
158
164
|
return trailingSlash.toggle(key, entryCreatesSubtree);
|
|
159
165
|
}
|
|
160
166
|
|
|
161
|
-
function keys(object, eagerProperties, entries) {
|
|
162
|
-
return entries
|
|
167
|
+
function keys(object, eagerProperties, propertyIsEnumerable, entries) {
|
|
168
|
+
return entries
|
|
169
|
+
.filter(([key]) => propertyIsEnumerable[key])
|
|
170
|
+
.map((entry) => entryKey(object, eagerProperties, entry));
|
|
163
171
|
}
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
isPlainObject,
|
|
3
3
|
isUnpackable,
|
|
4
4
|
merge,
|
|
5
|
+
setParent,
|
|
5
6
|
Tree,
|
|
6
7
|
} from "@weborigami/async-tree";
|
|
7
8
|
|
|
@@ -59,5 +60,6 @@ export default async function mergeTrees(...trees) {
|
|
|
59
60
|
|
|
60
61
|
// Merge the trees.
|
|
61
62
|
const result = merge(...unpacked);
|
|
63
|
+
setParent(result, this);
|
|
62
64
|
return result;
|
|
63
65
|
}
|
package/src/runtime/ops.js
CHANGED
|
@@ -33,6 +33,17 @@ export function addition(a, b) {
|
|
|
33
33
|
}
|
|
34
34
|
addOpLabel(addition, "«ops.addition»");
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Construct an array.
|
|
38
|
+
*
|
|
39
|
+
* @this {AsyncTree|null}
|
|
40
|
+
* @param {any[]} items
|
|
41
|
+
*/
|
|
42
|
+
export async function array(...items) {
|
|
43
|
+
return items;
|
|
44
|
+
}
|
|
45
|
+
addOpLabel(array, "«ops.array»");
|
|
46
|
+
|
|
36
47
|
export function bitwiseAnd(a, b) {
|
|
37
48
|
return a & b;
|
|
38
49
|
}
|
|
@@ -53,17 +64,6 @@ export function bitwiseXor(a, b) {
|
|
|
53
64
|
}
|
|
54
65
|
addOpLabel(bitwiseXor, "«ops.bitwiseXor»");
|
|
55
66
|
|
|
56
|
-
/**
|
|
57
|
-
* Construct an array.
|
|
58
|
-
*
|
|
59
|
-
* @this {AsyncTree|null}
|
|
60
|
-
* @param {any[]} items
|
|
61
|
-
*/
|
|
62
|
-
export async function array(...items) {
|
|
63
|
-
return items;
|
|
64
|
-
}
|
|
65
|
-
addOpLabel(array, "«ops.array»");
|
|
66
|
-
|
|
67
67
|
/**
|
|
68
68
|
* Like ops.scope, but only searches for a builtin at the top of the scope
|
|
69
69
|
* chain.
|
|
@@ -328,9 +328,41 @@ export async function logicalOr(head, ...tail) {
|
|
|
328
328
|
* Merge the given trees. If they are all plain objects, return a plain object.
|
|
329
329
|
*
|
|
330
330
|
* @this {AsyncTree|null}
|
|
331
|
-
* @param {
|
|
331
|
+
* @param {Code[]} codes
|
|
332
332
|
*/
|
|
333
|
-
export async function merge(...
|
|
333
|
+
export async function merge(...codes) {
|
|
334
|
+
// First pass: evaluate the direct property entries in a single object
|
|
335
|
+
let treeSpreads = false;
|
|
336
|
+
const directEntries = [];
|
|
337
|
+
for (const code of codes) {
|
|
338
|
+
if (code[0] === object) {
|
|
339
|
+
directEntries.push(...code.slice(1));
|
|
340
|
+
} else {
|
|
341
|
+
treeSpreads = true;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const directObject = directEntries
|
|
346
|
+
? await expressionObject(directEntries, this)
|
|
347
|
+
: null;
|
|
348
|
+
if (!treeSpreads) {
|
|
349
|
+
// No tree spreads, we're done
|
|
350
|
+
return directObject;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Second pass: evaluate the trees with the direct properties object in scope
|
|
354
|
+
let context;
|
|
355
|
+
if (directObject) {
|
|
356
|
+
context = Tree.from(directObject);
|
|
357
|
+
context.parent = this;
|
|
358
|
+
} else {
|
|
359
|
+
context = this;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const trees = await Promise.all(
|
|
363
|
+
codes.map(async (code) => evaluate.call(context, code))
|
|
364
|
+
);
|
|
365
|
+
|
|
334
366
|
return mergeTrees.call(this, ...trees);
|
|
335
367
|
}
|
|
336
368
|
addOpLabel(merge, "«ops.merge»");
|
|
@@ -438,7 +470,7 @@ addOpLabel(shiftRightUnsigned, "«ops.shiftRightUnsigned»");
|
|
|
438
470
|
* The spread operator is a placeholder during parsing. It should be replaced
|
|
439
471
|
* with an object merge.
|
|
440
472
|
*/
|
|
441
|
-
export function spread(
|
|
473
|
+
export function spread(arg) {
|
|
442
474
|
throw new Error(
|
|
443
475
|
"Internal error: a spread operation wasn't compiled correctly."
|
|
444
476
|
);
|
package/src/runtime/typos.js
CHANGED
|
@@ -15,6 +15,12 @@ export function isTypo(s1, s2) {
|
|
|
15
15
|
return false;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// If strings are both a single character, we don't want to consider them
|
|
19
|
+
// typos.
|
|
20
|
+
if (length1 === 1 && length2 === 1) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
// If length difference is more than 1, distance can't be 1
|
|
19
25
|
if (Math.abs(length1 - length2) > 1) {
|
|
20
26
|
return false;
|
|
@@ -701,6 +701,11 @@ describe("Origami parser", () => {
|
|
|
701
701
|
[ops.object, ["a", [ops.literal, 1]]],
|
|
702
702
|
[ops.scope, "b"],
|
|
703
703
|
]);
|
|
704
|
+
assertParse("objectLiteral", "{ a: 1, ...{ b: 2 } }", [
|
|
705
|
+
ops.object,
|
|
706
|
+
["a", [ops.literal, 1]],
|
|
707
|
+
["b", [ops.literal, 2]],
|
|
708
|
+
]);
|
|
704
709
|
assertParse("objectLiteral", "{ (a): 1 }", [
|
|
705
710
|
ops.object,
|
|
706
711
|
["(a)", [ops.literal, 1]],
|
package/test/runtime/ops.test.js
CHANGED
|
@@ -220,6 +220,45 @@ describe("ops", () => {
|
|
|
220
220
|
assert.strictEqual(await ops.logicalOr(true, errorFn), true);
|
|
221
221
|
});
|
|
222
222
|
|
|
223
|
+
test("ops.merge", async () => {
|
|
224
|
+
// {
|
|
225
|
+
// a: 1
|
|
226
|
+
// …fn(a)
|
|
227
|
+
// }
|
|
228
|
+
const scope = new ObjectTree({
|
|
229
|
+
fn: (a) => ({ b: 2 * a }),
|
|
230
|
+
});
|
|
231
|
+
const code = createCode([
|
|
232
|
+
ops.merge,
|
|
233
|
+
[ops.object, ["a", [ops.literal, 1]]],
|
|
234
|
+
[
|
|
235
|
+
[ops.builtin, "fn"],
|
|
236
|
+
[ops.scope, "a"],
|
|
237
|
+
],
|
|
238
|
+
]);
|
|
239
|
+
const result = await evaluate.call(scope, code);
|
|
240
|
+
assert.deepEqual(result, { a: 1, b: 2 });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("ops.merge lets all direct properties see each other", async () => {
|
|
244
|
+
// {
|
|
245
|
+
// a: 1
|
|
246
|
+
// ...more
|
|
247
|
+
// c: a
|
|
248
|
+
// }
|
|
249
|
+
const scope = new ObjectTree({
|
|
250
|
+
more: { b: 2 },
|
|
251
|
+
});
|
|
252
|
+
const code = createCode([
|
|
253
|
+
ops.merge,
|
|
254
|
+
[ops.object, ["a", [ops.literal, 1]]],
|
|
255
|
+
[ops.scope, "more"],
|
|
256
|
+
[ops.object, ["c", [ops.scope, "a"]]],
|
|
257
|
+
]);
|
|
258
|
+
const result = await evaluate.call(scope, code);
|
|
259
|
+
assert.deepEqual(result, { a: 1, b: 2, c: 1 });
|
|
260
|
+
});
|
|
261
|
+
|
|
223
262
|
test("ops.multiplication multiplies two numbers", async () => {
|
|
224
263
|
assert.strictEqual(ops.multiplication(3, 4), 12);
|
|
225
264
|
assert.strictEqual(ops.multiplication(-3, 4), -12);
|
|
@@ -12,6 +12,7 @@ describe("typos", () => {
|
|
|
12
12
|
assert(isTypo("cat", "cta")); // transposition
|
|
13
13
|
assert(isTypo("cat", "act")); // transposition
|
|
14
14
|
assert(!isTypo("cat", "dog")); // more than 1 edit
|
|
15
|
+
assert(!isTypo("a", "b")); // single character
|
|
15
16
|
});
|
|
16
17
|
|
|
17
18
|
test("typos", () => {
|