@weborigami/language 0.0.50 → 0.0.52

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.
@@ -17,6 +17,38 @@ export function makeFunctionCall(target, chain) {
17
17
  return value;
18
18
  }
19
19
 
20
+ export function makeObject(entries, op) {
21
+ let currentEntries = [];
22
+ const spreads = [];
23
+
24
+ for (const [key, value] of entries) {
25
+ if (key === ops.spread) {
26
+ if (currentEntries.length > 0) {
27
+ spreads.push([op, ...currentEntries]);
28
+ currentEntries = [];
29
+ }
30
+ spreads.push(value);
31
+ } else {
32
+ currentEntries.push([key, value]);
33
+ }
34
+ }
35
+
36
+ // Finish any current entries.
37
+ if (currentEntries.length > 0) {
38
+ spreads.push([op, ...currentEntries]);
39
+ currentEntries = [];
40
+ }
41
+
42
+ if (spreads.length > 1) {
43
+ return [ops.merge, ...spreads];
44
+ }
45
+ if (spreads.length === 1) {
46
+ return spreads[0];
47
+ } else {
48
+ return [op];
49
+ }
50
+ }
51
+
20
52
  // Similar to a function call, but the order is reversed.
21
53
  export function makePipeline(steps) {
22
54
  const [first, ...rest] = steps;
@@ -0,0 +1,73 @@
1
+ import { isPlainObject, isUnpackable, merge } from "@weborigami/async-tree";
2
+ import Scope from "./Scope.js";
3
+
4
+ /**
5
+ * Create a tree that's the result of merging the given trees.
6
+ *
7
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
8
+ * @typedef {import("@weborigami/async-tree").Treelike} Treelike
9
+ *
10
+ * @this {AsyncTree|null}
11
+ * @param {(Treelike|null)[]} trees
12
+ */
13
+ export default async function mergeTrees(...trees) {
14
+ // Filter out null or undefined trees.
15
+ /** @type {Treelike[]}
16
+ * @ts-ignore */
17
+ const filtered = trees.filter((tree) => tree);
18
+
19
+ if (filtered.length === 1) {
20
+ // Only one tree, no need to merge.
21
+ return filtered[0];
22
+ }
23
+
24
+ // Unpack any packed objects.
25
+ const unpacked = await Promise.all(
26
+ filtered.map((obj) =>
27
+ isUnpackable(obj) ? /** @type {any} */ (obj).unpack() : obj
28
+ )
29
+ );
30
+
31
+ // If all trees are plain objects, return a plain object.
32
+ if (unpacked.every((tree) => isPlainObject(tree))) {
33
+ return mergeObjects(...unpacked);
34
+ }
35
+
36
+ // If a tree can take a scope, give it one that includes the other trees and
37
+ // the current scope.
38
+ const scopedTrees = unpacked.map((tree) => {
39
+ const otherTrees = unpacked.filter((g) => g !== tree);
40
+ const scope = new Scope(...otherTrees, this);
41
+ // Each tree will be included first in its own scope.
42
+ return Scope.treeWithScope(tree, scope);
43
+ });
44
+
45
+ // Merge the trees.
46
+ const result = merge(...scopedTrees);
47
+
48
+ // Give the overall mixed tree a scope that includes the component trees and
49
+ // the current scope.
50
+ /** @type {any} */ (result).scope = new Scope(result, this);
51
+
52
+ return result;
53
+ }
54
+
55
+ /**
56
+ * Merge the indicated plain objects. If a key is present in multiple objects,
57
+ * the value from the first object is used.
58
+ *
59
+ * This is similar to calling Object.assign() with the objects in reverse order,
60
+ * but we want to ensure the keys end up in the same order they're encountered
61
+ * in the objects.
62
+ *
63
+ * @param {...any} objects
64
+ */
65
+ function mergeObjects(...objects) {
66
+ const result = {};
67
+ for (const obj of objects) {
68
+ for (const key of Object.keys(obj)) {
69
+ result[key] ??= obj[key];
70
+ }
71
+ }
72
+ return result;
73
+ }
@@ -10,6 +10,7 @@ import Scope from "./Scope.js";
10
10
  import concatTreeValues from "./concatTreeValues.js";
11
11
  import handleExtension from "./handleExtension.js";
12
12
  import { OrigamiTree, evaluate, expressionFunction } from "./internal.js";
13
+ import mergeTrees from "./mergeTrees.js";
13
14
 
14
15
  // For memoizing lambda functions
15
16
  const lambdaFnMap = new Map();
@@ -209,6 +210,17 @@ export function lambda(parameters, code) {
209
210
  }
210
211
  lambda.toString = () => "«ops.lambda»";
211
212
 
213
+ /**
214
+ * Merge the given trees. If they are all plain objects, return a plain object.
215
+ *
216
+ * @this {AsyncTree|null}
217
+ * @param {import("@weborigami/async-tree").Treelike[]} trees
218
+ */
219
+ export async function merge(...trees) {
220
+ return mergeTrees.call(this, ...trees);
221
+ }
222
+ merge.toString = () => "«ops.merge»";
223
+
212
224
  /**
213
225
  * Construct an object. The keys will be the same as the given `obj`
214
226
  * parameter's, and the values will be the results of evaluating the
@@ -228,6 +240,17 @@ export async function object(...entries) {
228
240
  }
229
241
  object.toString = () => "«ops.object»";
230
242
 
243
+ /**
244
+ * The spread operator is a placeholder during parsing. It should be replaced
245
+ * with an object merge.
246
+ */
247
+ export function spread(...args) {
248
+ throw new Error(
249
+ "A compile-time spread operator wasn't converted to an object merge."
250
+ );
251
+ }
252
+ spread.toString = () => "«ops.spread»";
253
+
231
254
  /**
232
255
  * Traverse a path of keys through a tree.
233
256
  */
@@ -19,23 +19,20 @@ describe("Origami parser", () => {
19
19
  assertParse("array", "[ 1 , 2 , 3 ]", [ops.array, 1, 2, 3]);
20
20
  });
21
21
 
22
- test("assignment", () => {
23
- assertParse("assignment", "data = obj.json", [
22
+ test("treeAssignment", () => {
23
+ assertParse("treeAssignment", "data = obj.json", [
24
24
  "data",
25
25
  [ops.scope, "obj.json"],
26
26
  ]);
27
- assertParse("assignment", "foo = fn 'bar'", [
27
+ assertParse("treeAssignment", "foo = fn 'bar'", [
28
28
  "foo",
29
29
  [[ops.scope, "fn"], "bar"],
30
30
  ]);
31
31
  });
32
32
 
33
- test("assignmentOrShorthand", () => {
34
- assertParse("assignmentOrShorthand", "foo", [
35
- "foo",
36
- [ops.inherited, "foo"],
37
- ]);
38
- assertParse("assignmentOrShorthand", "foo = 1", ["foo", 1]);
33
+ test("treeEntry", () => {
34
+ assertParse("treeEntry", "foo", ["foo", [ops.inherited, "foo"]]);
35
+ assertParse("treeEntry", "foo = 1", ["foo", 1]);
39
36
  });
40
37
 
41
38
  test("expr", () => {
@@ -297,6 +294,11 @@ describe("Origami parser", () => {
297
294
  ["a", 1],
298
295
  ["b", [ops.scope, "b"]],
299
296
  ]);
297
+ assertParse("object", "{ a: 1, ...b }", [
298
+ ops.merge,
299
+ [ops.object, ["a", 1]],
300
+ [ops.scope, "b"],
301
+ ]);
300
302
  });
301
303
 
302
304
  test("objectProperty", () => {
@@ -308,12 +310,9 @@ describe("Origami parser", () => {
308
310
  ]);
309
311
  });
310
312
 
311
- test("objectPropertyOrShorthand", () => {
312
- assertParse("objectPropertyOrShorthand", "foo", [
313
- "foo",
314
- [ops.scope, "foo"],
315
- ]);
316
- assertParse("objectPropertyOrShorthand", "x: y", ["x", [ops.scope, "y"]]);
313
+ test("objectEntry", () => {
314
+ assertParse("objectEntry", "foo", ["foo", [ops.scope, "foo"]]);
315
+ assertParse("objectEntry", "x: y", ["x", [ops.scope, "y"]]);
317
316
  });
318
317
 
319
318
  test("parameterizedLambda", () => {
@@ -410,6 +409,11 @@ describe("Origami parser", () => {
410
409
  assertParse("scopeReference", "x", [ops.scope, "x"]);
411
410
  });
412
411
 
412
+ test("spread", () => {
413
+ assertParse("spread", "...a", [ops.spread, [ops.scope, "a"]]);
414
+ assertParse("spread", "…a", [ops.spread, [ops.scope, "a"]]);
415
+ });
416
+
413
417
  test("string", () => {
414
418
  assertParse("string", '"foo"', "foo");
415
419
  assertParse("string", "'bar'", "bar");
@@ -485,6 +489,11 @@ describe("Origami parser", () => {
485
489
  ops.tree,
486
490
  ["x", [[ops.scope, "fn"], "a"]],
487
491
  ]);
492
+ assertParse("tree", "{ a = 1, ...b }", [
493
+ ops.merge,
494
+ [ops.tree, ["a", 1]],
495
+ [ops.scope, "b"],
496
+ ]);
488
497
  });
489
498
 
490
499
  test("whitespace block", () => {
@@ -0,0 +1,69 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+ import { ExpressionTree, expressionFunction, ops } from "@weborigami/language";
3
+ import assert from "node:assert";
4
+ import { describe, test } from "node:test";
5
+ import mergeTrees from "../../src/runtime/mergeTrees.js";
6
+
7
+ describe("mergeTrees", () => {
8
+ test("merges trees", async () => {
9
+ const tree = await mergeTrees.call(
10
+ null,
11
+ {
12
+ a: 1,
13
+ b: 2,
14
+ },
15
+ {
16
+ c: 3,
17
+ d: 4,
18
+ }
19
+ );
20
+ // @ts-ignore
21
+ assert.deepEqual(await Tree.plain(tree), {
22
+ a: 1,
23
+ b: 2,
24
+ c: 3,
25
+ d: 4,
26
+ });
27
+ });
28
+
29
+ test("puts all trees in scope", async () => {
30
+ const tree = await mergeTrees.call(
31
+ null,
32
+ new ExpressionTree({
33
+ a: 1,
34
+ b: expressionFunction.createExpressionFunction([ops.scope, "c"]),
35
+ }),
36
+ new ExpressionTree({
37
+ c: 2,
38
+ d: expressionFunction.createExpressionFunction([ops.scope, "a"]),
39
+ })
40
+ );
41
+ // @ts-ignore
42
+ assert.deepEqual(await Tree.plain(tree), {
43
+ a: 1,
44
+ b: 2,
45
+ c: 2,
46
+ d: 1,
47
+ });
48
+ });
49
+
50
+ test("if all arguments are plain objects, result is a plain object", async () => {
51
+ const result = await mergeTrees.call(
52
+ null,
53
+ {
54
+ a: 1,
55
+ b: 2,
56
+ },
57
+ {
58
+ c: 3,
59
+ d: 4,
60
+ }
61
+ );
62
+ assert.deepEqual(result, {
63
+ a: 1,
64
+ b: 2,
65
+ c: 3,
66
+ d: 4,
67
+ });
68
+ });
69
+ });