@weborigami/language 0.0.51 → 0.0.53

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.
@@ -2,6 +2,38 @@ import * as ops from "../runtime/ops.js";
2
2
 
3
3
  // Parser helpers
4
4
 
5
+ export function makeArray(entries) {
6
+ let currentEntries = [];
7
+ const spreads = [];
8
+
9
+ for (const value of entries) {
10
+ if (Array.isArray(value) && value[0] === ops.spread) {
11
+ if (currentEntries.length > 0) {
12
+ spreads.push([ops.array, ...currentEntries]);
13
+ currentEntries = [];
14
+ }
15
+ spreads.push(...value.slice(1));
16
+ } else {
17
+ currentEntries.push(value);
18
+ }
19
+ }
20
+
21
+ // Finish any current entries.
22
+ if (currentEntries.length > 0) {
23
+ spreads.push([ops.array, ...currentEntries]);
24
+ currentEntries = [];
25
+ }
26
+
27
+ if (spreads.length > 1) {
28
+ return [ops.merge, ...spreads];
29
+ }
30
+ if (spreads.length === 1) {
31
+ return spreads[0];
32
+ } else {
33
+ return [ops.array];
34
+ }
35
+ }
36
+
5
37
  export function makeFunctionCall(target, chain) {
6
38
  let value = target;
7
39
  // The chain is an array of arguments (which are themselves arrays). We
@@ -17,6 +49,38 @@ export function makeFunctionCall(target, chain) {
17
49
  return value;
18
50
  }
19
51
 
52
+ export function makeObject(entries, op) {
53
+ let currentEntries = [];
54
+ const spreads = [];
55
+
56
+ for (const [key, value] of entries) {
57
+ if (key === ops.spread) {
58
+ if (currentEntries.length > 0) {
59
+ spreads.push([op, ...currentEntries]);
60
+ currentEntries = [];
61
+ }
62
+ spreads.push(value);
63
+ } else {
64
+ currentEntries.push([key, value]);
65
+ }
66
+ }
67
+
68
+ // Finish any current entries.
69
+ if (currentEntries.length > 0) {
70
+ spreads.push([op, ...currentEntries]);
71
+ currentEntries = [];
72
+ }
73
+
74
+ if (spreads.length > 1) {
75
+ return [ops.merge, ...spreads];
76
+ }
77
+ if (spreads.length === 1) {
78
+ return spreads[0];
79
+ } else {
80
+ return [op];
81
+ }
82
+ }
83
+
20
84
  // Similar to a function call, but the order is reversed.
21
85
  export function makePipeline(steps) {
22
86
  const [first, ...rest] = steps;
@@ -0,0 +1,78 @@
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 all trees are arrays, return an array.
37
+ if (unpacked.every((tree) => Array.isArray(tree))) {
38
+ return unpacked.flat();
39
+ }
40
+
41
+ // If a tree can take a scope, give it one that includes the other trees and
42
+ // the current scope.
43
+ const scopedTrees = unpacked.map((tree) => {
44
+ const otherTrees = unpacked.filter((g) => g !== tree);
45
+ const scope = new Scope(...otherTrees, this);
46
+ // Each tree will be included first in its own scope.
47
+ return Scope.treeWithScope(tree, scope);
48
+ });
49
+
50
+ // Merge the trees.
51
+ const result = merge(...scopedTrees);
52
+
53
+ // Give the overall mixed tree a scope that includes the component trees and
54
+ // the current scope.
55
+ /** @type {any} */ (result).scope = new Scope(result, this);
56
+
57
+ return result;
58
+ }
59
+
60
+ /**
61
+ * Merge the indicated plain objects. If a key is present in multiple objects,
62
+ * the value from the first object is used.
63
+ *
64
+ * This is similar to calling Object.assign() with the objects in reverse order,
65
+ * but we want to ensure the keys end up in the same order they're encountered
66
+ * in the objects.
67
+ *
68
+ * @param {...any} objects
69
+ */
70
+ function mergeObjects(...objects) {
71
+ const result = {};
72
+ for (const obj of objects) {
73
+ for (const key of Object.keys(obj)) {
74
+ result[key] ??= obj[key];
75
+ }
76
+ }
77
+ return result;
78
+ }
@@ -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
  */
@@ -17,25 +17,27 @@ describe("Origami parser", () => {
17
17
  assertParse("array", "[]", [ops.array]);
18
18
  assertParse("array", "[1, 2, 3]", [ops.array, 1, 2, 3]);
19
19
  assertParse("array", "[ 1 , 2 , 3 ]", [ops.array, 1, 2, 3]);
20
+ assertParse("array", "[ 1, ...[2, 3]]", [
21
+ ops.merge,
22
+ [ops.array, 1],
23
+ [ops.array, 2, 3],
24
+ ]);
20
25
  });
21
26
 
22
- test("assignment", () => {
23
- assertParse("assignment", "data = obj.json", [
27
+ test("treeAssignment", () => {
28
+ assertParse("treeAssignment", "data = obj.json", [
24
29
  "data",
25
30
  [ops.scope, "obj.json"],
26
31
  ]);
27
- assertParse("assignment", "foo = fn 'bar'", [
32
+ assertParse("treeAssignment", "foo = fn 'bar'", [
28
33
  "foo",
29
34
  [[ops.scope, "fn"], "bar"],
30
35
  ]);
31
36
  });
32
37
 
33
- test("assignmentOrShorthand", () => {
34
- assertParse("assignmentOrShorthand", "foo", [
35
- "foo",
36
- [ops.inherited, "foo"],
37
- ]);
38
- assertParse("assignmentOrShorthand", "foo = 1", ["foo", 1]);
38
+ test("treeEntry", () => {
39
+ assertParse("treeEntry", "foo", ["foo", [ops.inherited, "foo"]]);
40
+ assertParse("treeEntry", "foo = 1", ["foo", 1]);
39
41
  });
40
42
 
41
43
  test("expr", () => {
@@ -297,6 +299,11 @@ describe("Origami parser", () => {
297
299
  ["a", 1],
298
300
  ["b", [ops.scope, "b"]],
299
301
  ]);
302
+ assertParse("object", "{ a: 1, ...b }", [
303
+ ops.merge,
304
+ [ops.object, ["a", 1]],
305
+ [ops.scope, "b"],
306
+ ]);
300
307
  });
301
308
 
302
309
  test("objectProperty", () => {
@@ -308,12 +315,9 @@ describe("Origami parser", () => {
308
315
  ]);
309
316
  });
310
317
 
311
- test("objectPropertyOrShorthand", () => {
312
- assertParse("objectPropertyOrShorthand", "foo", [
313
- "foo",
314
- [ops.scope, "foo"],
315
- ]);
316
- assertParse("objectPropertyOrShorthand", "x: y", ["x", [ops.scope, "y"]]);
318
+ test("objectEntry", () => {
319
+ assertParse("objectEntry", "foo", ["foo", [ops.scope, "foo"]]);
320
+ assertParse("objectEntry", "x: y", ["x", [ops.scope, "y"]]);
317
321
  });
318
322
 
319
323
  test("parameterizedLambda", () => {
@@ -410,6 +414,11 @@ describe("Origami parser", () => {
410
414
  assertParse("scopeReference", "x", [ops.scope, "x"]);
411
415
  });
412
416
 
417
+ test("spread", () => {
418
+ assertParse("spread", "...a", [ops.spread, [ops.scope, "a"]]);
419
+ assertParse("spread", "…a", [ops.spread, [ops.scope, "a"]]);
420
+ });
421
+
413
422
  test("string", () => {
414
423
  assertParse("string", '"foo"', "foo");
415
424
  assertParse("string", "'bar'", "bar");
@@ -485,6 +494,11 @@ describe("Origami parser", () => {
485
494
  ops.tree,
486
495
  ["x", [[ops.scope, "fn"], "a"]],
487
496
  ]);
497
+ assertParse("tree", "{ a = 1, ...b }", [
498
+ ops.merge,
499
+ [ops.tree, ["a", 1]],
500
+ [ops.scope, "b"],
501
+ ]);
488
502
  });
489
503
 
490
504
  test("whitespace block", () => {
@@ -0,0 +1,74 @@
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
+
70
+ test("if all arguments are arrays, result is an array", async () => {
71
+ const result = await mergeTrees.call(null, [1, 2], [3, 4]);
72
+ assert.deepEqual(result, [1, 2, 3, 4]);
73
+ });
74
+ });