@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/language",
3
- "version": "0.2.6",
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.6",
16
- "@weborigami/types": "0.2.6",
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
- // Spread entry; accumulate
239
- if (currentEntries.length > 0) {
240
- spreads.push([op, ...currentEntries]);
241
- currentEntries = [];
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]);
@@ -20,7 +20,13 @@ export default async function evaluate(code) {
20
20
  }
21
21
 
22
22
  let evaluated;
23
- const unevaluatedFns = [ops.external, ops.lambda, ops.object, ops.literal];
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 regular property.
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.map((entry) => entryKey(object, eagerProperties, entry));
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
  }
@@ -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 {import("@weborigami/async-tree").Treelike[]} trees
331
+ * @param {Code[]} codes
332
332
  */
333
- export async function merge(...trees) {
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(...args) {
473
+ export function spread(arg) {
442
474
  throw new Error(
443
475
  "Internal error: a spread operation wasn't compiled correctly."
444
476
  );
@@ -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]],
@@ -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", () => {