@weborigami/language 0.0.41 → 0.0.43

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,16 @@ export function makeFunctionCall(target, chain) {
17
17
  return value;
18
18
  }
19
19
 
20
+ // Similar to a function call, but the order is reversed.
21
+ export function makePipeline(steps) {
22
+ const [first, ...rest] = steps;
23
+ let value = first;
24
+ for (const args of rest) {
25
+ value = [args, value];
26
+ }
27
+ return value;
28
+ }
29
+
20
30
  export function makeTemplate(parts) {
21
31
  // Drop empty/null strings.
22
32
  const filtered = parts.filter((part) => part);
@@ -4,6 +4,9 @@ import {
4
4
  isPlainObject,
5
5
  } from "@weborigami/async-tree";
6
6
 
7
+ const textDecoder = new TextDecoder();
8
+ const TypedArray = Object.getPrototypeOf(Uint8Array);
9
+
7
10
  /**
8
11
  * Concatenate the text values in a tree.
9
12
  *
@@ -52,6 +55,9 @@ async function getText(value, scope) {
52
55
  text = "";
53
56
  } else if (typeof value === "string") {
54
57
  text = value;
58
+ } else if (value instanceof ArrayBuffer || value instanceof TypedArray) {
59
+ // Serialize data as UTF-8.
60
+ text = textDecoder.decode(value);
55
61
  } else if (
56
62
  !(value instanceof Array) &&
57
63
  value.toString !== getRealmObjectPrototype(value).toString
@@ -1,7 +1,8 @@
1
1
  import { Tree, isPlainObject } from "@weborigami/async-tree";
2
- import { format, ops } from "./internal.js";
2
+ import { ops } from "./internal.js";
3
3
 
4
- const expressionSymbol = Symbol("expression");
4
+ const codeSymbol = Symbol("code");
5
+ const sourceSymbol = Symbol("source");
5
6
 
6
7
  /**
7
8
  * Evaluate the given code and return the result.
@@ -9,10 +10,9 @@ const expressionSymbol = Symbol("expression");
9
10
  * `this` should be the scope used to look up references found in the code.
10
11
  *
11
12
  * @typedef {import("@weborigami/async-tree").Treelike} Treelike
12
- * @typedef {import("../../../language/src/compiler/code.js").Code} Code
13
13
  *
14
14
  * @this {Treelike|null}
15
- * @param {Code} code
15
+ * @param {any} code
16
16
  */
17
17
  export default async function evaluate(code) {
18
18
  const scope = this;
@@ -42,9 +42,7 @@ export default async function evaluate(code) {
42
42
 
43
43
  if (!fn) {
44
44
  // The code wants to invoke something that's couldn't be found in scope.
45
- throw ReferenceError(
46
- `Couldn't find function or tree key: ${format(code[0])}`
47
- );
45
+ throw ReferenceError(`${codeFragment(code[0])} is not defined`);
48
46
  }
49
47
 
50
48
  if (
@@ -57,9 +55,7 @@ export default async function evaluate(code) {
57
55
 
58
56
  if (!Tree.isTreelike(fn)) {
59
57
  throw TypeError(
60
- `Expect to invoke a function or a tree but instead got: ${format(
61
- code[0]
62
- )}`
58
+ `${codeFragment(code[0])} didn't return a function that can be called`
63
59
  );
64
60
  }
65
61
 
@@ -71,26 +67,34 @@ export default async function evaluate(code) {
71
67
  ? await fn.call(scope, ...args) // Invoke the function
72
68
  : await Tree.traverseOrThrow(fn, ...args); // Traverse the tree.
73
69
  } catch (/** @type {any} */ error) {
74
- const message = `Error triggered by Origami expression: ${format(code)}`;
75
- throw new Error(message, { cause: error });
70
+ if (!error.location) {
71
+ // Attach the location of the code we were evaluating.
72
+ error.location = /** @type {any} */ (code).location;
73
+ }
74
+ throw error;
76
75
  }
77
76
 
78
- // To aid debugging, add the expression source to the result.
77
+ // To aid debugging, add the code to the result.
79
78
  if (
80
79
  result &&
81
80
  typeof result === "object" &&
82
81
  Object.isExtensible(result) &&
83
82
  !isPlainObject(result)
84
83
  ) {
85
- try {
86
- result[expressionSymbol] = format(code);
87
- } catch (error) {
88
- // Setting a Symbol-keyed property on some objects fails with `TypeError:
89
- // Cannot convert a Symbol value to a string` but it's unclear why
90
- // implicit casting of the symbol to a string occurs. Since this is not a
91
- // vital operation, we ignore such errors.
84
+ result[codeSymbol] = code;
85
+ if (/** @type {any} */ (code).location) {
86
+ result[sourceSymbol] = codeFragment(code);
92
87
  }
93
88
  }
94
89
 
95
90
  return result;
96
91
  }
92
+
93
+ function codeFragment(code) {
94
+ if (code.location) {
95
+ const { source, start, end } = code.location;
96
+ return source.text.slice(start.offset, end.offset);
97
+ } else {
98
+ return "";
99
+ }
100
+ }
@@ -1,11 +1,11 @@
1
1
  /** @typedef {import("@weborigami/types").AsyncTree} AsyncTree */
2
2
 
3
- import { evaluate, format } from "./internal.js";
3
+ import { evaluate } from "./internal.js";
4
4
 
5
5
  /**
6
6
  * Given parsed Origami code, return a function that executes that code.
7
7
  *
8
- * @param {string|number|Array} code - parsed Origami code
8
+ * @param {import("../../index.js").Code} code - parsed Origami code
9
9
  * @param {string} [name] - optional name of the function
10
10
  */
11
11
  export function createExpressionFunction(code, name) {
@@ -17,8 +17,7 @@ export function createExpressionFunction(code, name) {
17
17
  Object.defineProperty(fn, "name", { value: name });
18
18
  }
19
19
  fn.code = code;
20
- fn.source = format(code);
21
- fn.toString = () => fn.source;
20
+ fn.toString = () => code.source?.text;
22
21
  return fn;
23
22
  }
24
23
 
@@ -0,0 +1,52 @@
1
+ // Text we look for in an error stack to guess whether a given line represents a
2
+ // function in the Origami source code.
3
+ const origamiSourceSignals = [
4
+ "async-tree/src/",
5
+ "language/src/",
6
+ "origami/src/",
7
+ "at Scope.evaluate",
8
+ ];
9
+
10
+ /**
11
+ * Format an error for display in the console.
12
+ *
13
+ * @param {Error} error
14
+ */
15
+ export default function formatError(error) {
16
+ let message;
17
+ if (error.stack) {
18
+ // Display the stack only until we reach the Origami source code.
19
+ message = "";
20
+ let lines = error.stack.split("\n");
21
+ for (let i = 0; i < lines.length; i++) {
22
+ const line = lines[i];
23
+ if (origamiSourceSignals.some((signal) => line.includes(signal))) {
24
+ break;
25
+ }
26
+ if (message) {
27
+ message += "\n";
28
+ }
29
+ message += lines[i];
30
+ }
31
+ } else {
32
+ message = error.toString();
33
+ }
34
+
35
+ // Add location
36
+ let location = /** @type {any} */ (error).location;
37
+ if (location) {
38
+ let { source, start, end } = location;
39
+ let fragment = source.text.slice(start.offset, end.offset);
40
+ if (fragment.length === 0) {
41
+ // Use entire source.
42
+ fragment = source.text;
43
+ }
44
+ message += `\nevaluating: ${fragment}`;
45
+ if (typeof source === "object" && source.url) {
46
+ message += `\n at ${source.url.href}:${start.line}:${start.column}`;
47
+ } else if (source.text.includes("\n")) {
48
+ message += `\n at line ${start.line}, column ${start.column}`;
49
+ }
50
+ }
51
+ return message;
52
+ }
@@ -11,8 +11,6 @@ export * as ops from "./ops.js";
11
11
 
12
12
  export { default as evaluate } from "./evaluate.js";
13
13
 
14
- export { default as format } from "./format.js";
15
-
16
14
  export * as expressionFunction from "./expressionFunction.js";
17
15
 
18
16
  export { default as ExpressionTree } from "./ExpressionTree.js";
@@ -128,7 +128,7 @@ inherited.toString = () => "«ops.inherited»";
128
128
  /**
129
129
  * Return a function that will invoke the given code.
130
130
  *
131
- * @typedef {import("../../../language/src/compiler/code.js").Code} Code
131
+ * @typedef {import("../../index.ts").Code} Code
132
132
  * @this {AsyncTree|null}
133
133
  * @param {string[]} parameters
134
134
  * @param {Code} code
@@ -49,7 +49,7 @@ describe("compile", () => {
49
49
  });
50
50
 
51
51
  test("templateLiteral", async () => {
52
- await assertCompile("`Hello, {{name}}!`", "Hello, Alice!");
52
+ await assertCompile("`Hello, ${name}!`", "Hello, Alice!");
53
53
  await assertCompile(
54
54
  "`escape characters with \\`backslash\\``",
55
55
  "escape characters with `backslash`"
@@ -1,3 +1,4 @@
1
+ import { isPlainObject } from "@weborigami/async-tree";
1
2
  import assert from "node:assert";
2
3
  import { describe, test } from "node:test";
3
4
  import { parse } from "../../src/compiler/parse.js";
@@ -15,6 +16,7 @@ describe("Origami parser", () => {
15
16
  test("array", () => {
16
17
  assertParse("array", "[]", [ops.array]);
17
18
  assertParse("array", "[1, 2, 3]", [ops.array, 1, 2, 3]);
19
+ assertParse("array", "[ 1 , 2 , 3 ]", [ops.array, 1, 2, 3]);
18
20
  });
19
21
 
20
22
  test("assignment", () => {
@@ -68,11 +70,15 @@ describe("Origami parser", () => {
68
70
  ],
69
71
  [[ops.scope, "files"], "snapshot"],
70
72
  ]);
71
- assertParse("expr", "@map =`<li>{{_}}</li>`", [
73
+ assertParse("expr", "@map =`<li>${_}</li>`", [
72
74
  [ops.scope, "@map"],
73
75
  [ops.lambda, null, [ops.concat, "<li>", [ops.scope, "_"], "</li>"]],
74
76
  ]);
75
77
  assertParse("expr", `"https://example.com"`, "https://example.com");
78
+ assertParse("expr", "'Hello' -> test.orit", [
79
+ [ops.scope, "test.orit"],
80
+ "Hello",
81
+ ]);
76
82
  });
77
83
 
78
84
  test("expression", () => {
@@ -103,6 +109,18 @@ describe("Origami parser", () => {
103
109
  ],
104
110
  ]
105
111
  );
112
+
113
+ // Consecutive slahes inside a path = empty string key
114
+ assertParse("expression", "path//key", [
115
+ ops.traverse,
116
+ [ops.scope, "path"],
117
+ "",
118
+ "key",
119
+ ]);
120
+ // Single slash at start of something = absolute file path
121
+ assertParse("expression", "/path", [[ops.filesRoot], "path"]);
122
+ // Consecutive slashes at start of something = comment
123
+ assertParse("expression", "path //comment", [ops.scope, "path"]);
106
124
  });
107
125
 
108
126
  test("functionComposition", () => {
@@ -116,6 +134,11 @@ describe("Origami parser", () => {
116
134
  [ops.scope, "a"],
117
135
  [ops.scope, "b"],
118
136
  ]);
137
+ assertParse("functionComposition", "fn( a , b )", [
138
+ [ops.scope, "fn"],
139
+ [ops.scope, "a"],
140
+ [ops.scope, "b"],
141
+ ]);
119
142
  assertParse("functionComposition", "fn()(arg)", [
120
143
  [[ops.scope, "fn"], undefined],
121
144
  [ops.scope, "arg"],
@@ -167,36 +190,16 @@ describe("Origami parser", () => {
167
190
  [ops.object, ["a", 1], ["b", 2]],
168
191
  "b",
169
192
  ]);
170
- });
171
-
172
- test("group", () => {
173
- assertParse("group", "(hello)", [ops.scope, "hello"]);
174
- assertParse("group", "(((nested)))", [ops.scope, "nested"]);
175
- assertParse("group", "(fn())", [[ops.scope, "fn"], undefined]);
176
- });
177
-
178
- test("host", () => {
179
- assertParse("host", "abc", "abc");
180
- assertParse("host", "abc:123", "abc:123");
181
- });
182
-
183
- test("identifier", () => {
184
- assertParse("identifier", "abc", "abc");
185
- assertParse("identifier", "index.html", "index.html");
186
- assertParse("identifier", "foo\\ bar", "foo bar");
187
- });
188
-
189
- test("implicitParensCall", () => {
190
- assertParse("implicitParensCall", "fn arg", [
193
+ assertParse("functionComposition", "fn arg", [
191
194
  [ops.scope, "fn"],
192
195
  [ops.scope, "arg"],
193
196
  ]);
194
- assertParse("implicitParensCall", "fn 'a', 'b'", [
197
+ assertParse("functionComposition", "fn 'a', 'b'", [
195
198
  [ops.scope, "fn"],
196
199
  "a",
197
200
  "b",
198
201
  ]);
199
- assertParse("implicitParensCall", "fn a(b), c", [
202
+ assertParse("functionComposition", "fn a(b), c", [
200
203
  [ops.scope, "fn"],
201
204
  [
202
205
  [ops.scope, "a"],
@@ -204,27 +207,49 @@ describe("Origami parser", () => {
204
207
  ],
205
208
  [ops.scope, "c"],
206
209
  ]);
207
- assertParse("implicitParensCall", "fn1 fn2 'arg'", [
210
+ assertParse("functionComposition", "fn1 fn2 'arg'", [
208
211
  [ops.scope, "fn1"],
209
212
  [[ops.scope, "fn2"], "arg"],
210
213
  ]);
211
- assertParse("implicitParensCall", "(fn()) 'arg'", [
214
+ assertParse("functionComposition", "(fn()) 'arg'", [
212
215
  [[ops.scope, "fn"], undefined],
213
216
  "arg",
214
217
  ]);
215
- assertParse("implicitParensCall", "https://example.com/tree.yaml 'key'", [
218
+ assertParse("functionComposition", "tree/key arg", [
219
+ [ops.traverse, [ops.scope, "tree"], "key"],
220
+ [ops.scope, "arg"],
221
+ ]);
222
+ assertParse("functionComposition", "https://example.com/tree.yaml 'key'", [
216
223
  [ops.https, "example.com", "tree.yaml"],
217
224
  "key",
218
225
  ]);
219
226
  });
220
227
 
228
+ test("group", () => {
229
+ assertParse("group", "(hello)", [ops.scope, "hello"]);
230
+ assertParse("group", "(((nested)))", [ops.scope, "nested"]);
231
+ assertParse("group", "(fn())", [[ops.scope, "fn"], undefined]);
232
+ });
233
+
234
+ test("host", () => {
235
+ assertParse("host", "abc", "abc");
236
+ assertParse("host", "abc:123", "abc:123");
237
+ });
238
+
239
+ test("identifier", () => {
240
+ assertParse("identifier", "abc", "abc");
241
+ assertParse("identifier", "index.html", "index.html");
242
+ assertParse("identifier", "foo\\ bar", "foo bar");
243
+ assertParse("identifier", "x-y-z", "x-y-z");
244
+ });
245
+
221
246
  test("lambda", () => {
222
247
  assertParse("lambda", "=message", [
223
248
  ops.lambda,
224
249
  null,
225
250
  [ops.scope, "message"],
226
251
  ]);
227
- assertParse("lambda", "=`Hello, {{name}}.`", [
252
+ assertParse("lambda", "=`Hello, ${name}.`", [
228
253
  ops.lambda,
229
254
  null,
230
255
  [ops.concat, "Hello, ", [ops.scope, "name"], "."],
@@ -235,7 +260,7 @@ describe("Origami parser", () => {
235
260
  assertParse("leadingSlashPath", "/tree/", ["tree", ""]);
236
261
  });
237
262
 
238
- describe("list", () => {
263
+ test("list", () => {
239
264
  assertParse("list", "1", [1]);
240
265
  assertParse("list", "1,2,3", [1, 2, 3]);
241
266
  assertParse("list", "1, 2, 3,", [1, 2, 3]);
@@ -244,6 +269,10 @@ describe("Origami parser", () => {
244
269
  assertParse("list", "'a' , 'b' , 'c'", ["a", "b", "c"]);
245
270
  });
246
271
 
272
+ test("multiLineComment", () => {
273
+ assertParse("multiLineComment", "/*\nHello, world!\n*/", null);
274
+ });
275
+
247
276
  test("number", () => {
248
277
  assertParse("number", "123", 123);
249
278
  assertParse("number", "-456", -456);
@@ -326,6 +355,20 @@ describe("Origami parser", () => {
326
355
  assertParse("path", "tree/foo/bar", ["tree", "foo", "bar"]);
327
356
  });
328
357
 
358
+ test("pipeline", () => {
359
+ assertParse("pipeline", "a -> b", [
360
+ [ops.scope, "b"],
361
+ [ops.scope, "a"],
362
+ ]);
363
+ assertParse("pipeline", "input → one.js → two.js", [
364
+ [ops.scope, "two.js"],
365
+ [
366
+ [ops.scope, "one.js"],
367
+ [ops.scope, "input"],
368
+ ],
369
+ ]);
370
+ });
371
+
329
372
  test("protocolCall", () => {
330
373
  assertParse("protocolCall", "foo://bar", [[ops.scope, "foo"], "bar"]);
331
374
  assertParse("protocolCall", "https://example.com/foo/", [
@@ -342,6 +385,14 @@ describe("Origami parser", () => {
342
385
  ]);
343
386
  });
344
387
 
388
+ test("singleLineComment", () => {
389
+ assertParse("singleLineComment", "# Hello, world!", null);
390
+ });
391
+
392
+ test("singleLineComment (JS)", () => {
393
+ assertParse("singleLineComment", "// Hello, world!", null);
394
+ });
395
+
345
396
  test("scopeReference", () => {
346
397
  assertParse("scopeReference", "x", [ops.scope, "x"]);
347
398
  });
@@ -356,7 +407,7 @@ describe("Origami parser", () => {
356
407
  });
357
408
 
358
409
  test("templateDocument", () => {
359
- assertParse("templateDocument", "hello{{foo}}world", [
410
+ assertParse("templateDocument", "hello${foo}world", [
360
411
  ops.lambda,
361
412
  null,
362
413
  [ops.concat, "hello", [ops.scope, "foo"], "world"],
@@ -387,10 +438,33 @@ describe("Origami parser", () => {
387
438
  ]);
388
439
  });
389
440
 
441
+ test("templateLiteral (JS)", () => {
442
+ assertParse("templateLiteral", "`Hello, world.`", "Hello, world.");
443
+ assertParse("templateLiteral", "`foo ${x} bar`", [
444
+ ops.concat,
445
+ "foo ",
446
+ [ops.scope, "x"],
447
+ " bar",
448
+ ]);
449
+ assertParse("templateLiteral", "`${`nested`}`", "nested");
450
+ assertParse("templateLiteral", "`${map(people, =`${name}`)}`", [
451
+ ops.concat,
452
+ [
453
+ [ops.scope, "map"],
454
+ [ops.scope, "people"],
455
+ [ops.lambda, null, [ops.concat, [ops.scope, "name"]]],
456
+ ],
457
+ ]);
458
+ });
459
+
390
460
  test("templateSubstitution", () => {
391
461
  assertParse("templateSubstitution", "{{foo}}", [ops.scope, "foo"]);
392
462
  });
393
463
 
464
+ test("templateSubtitution (JS)", () => {
465
+ assertParse("templateSubstitution", "${foo}", [ops.scope, "foo"]);
466
+ });
467
+
394
468
  test("tree", () => {
395
469
  assertParse("tree", "{}", [ops.tree]);
396
470
  assertParse("tree", "{ a = 1, b }", [
@@ -417,6 +491,25 @@ describe("Origami parser", () => {
417
491
  });
418
492
 
419
493
  function assertParse(startRule, source, expected) {
420
- const actual = parse(source, { startRule });
494
+ /** @type {any} */
495
+ const parseResult = parse(source, { grammarSource: source, startRule });
496
+ const actual = stripLocations(parseResult);
421
497
  assert.deepEqual(actual, expected);
422
498
  }
499
+
500
+ // For comparison purposes, strip the `location` property added by the parser.
501
+ function stripLocations(parseResult) {
502
+ if (Array.isArray(parseResult)) {
503
+ return parseResult.map(stripLocations);
504
+ } else if (isPlainObject(parseResult)) {
505
+ const result = {};
506
+ for (const key in parseResult) {
507
+ if (key !== "location") {
508
+ result[key] = stripLocations(parseResult[key]);
509
+ }
510
+ }
511
+ return result;
512
+ } else {
513
+ return parseResult;
514
+ }
515
+ }
@@ -1,3 +0,0 @@
1
- import type { Treelike } from "@weborigami/async-tree";
2
-
3
- type Code = [Treelike, ...any[]] | any;
@@ -1,127 +0,0 @@
1
- import { ops } from "./internal.js";
2
-
3
- export default function format(code, implicitFunctionCall = false) {
4
- if (code === null) {
5
- return "";
6
- } else if (typeof code === "string") {
7
- return `'${code}'`;
8
- } else if (typeof code === "symbol") {
9
- return `«${code.description}»`;
10
- } else if (!(code instanceof Array)) {
11
- return code;
12
- } else {
13
- switch (code[0]) {
14
- case ops.assign:
15
- return formatAssignment(code);
16
-
17
- case ops.concat:
18
- return formatTemplate(code);
19
-
20
- case ops.lambda:
21
- return formatLambda(code);
22
-
23
- case ops.object:
24
- return formatObject(code);
25
-
26
- case ops.scope:
27
- return formatScopeTraversal(code, implicitFunctionCall);
28
-
29
- case ops.tree:
30
- return formatTree(code);
31
-
32
- default:
33
- return code[0] instanceof Array
34
- ? formatFunctionCall(code)
35
- : "** Unknown Origami code **";
36
- }
37
- }
38
- }
39
-
40
- function formatArgument(arg) {
41
- return typeof arg === "string" ? `'${arg}'` : format(arg);
42
- }
43
-
44
- function formatArguments(args) {
45
- const allStrings = args.every((arg) => typeof arg === "string");
46
- return allStrings
47
- ? // Use tree traversal syntax.
48
- formatSlashPath(args)
49
- : // Use function invocation syntax.
50
- formatArgumentsList(args);
51
- }
52
-
53
- function formatArgumentsList(args) {
54
- const formatted = args.map((arg) => formatArgument(arg));
55
- const list = formatted.join(", ");
56
- return `(${list})`;
57
- }
58
-
59
- function formatAssignment(code) {
60
- const [_, declaration, expression] = code;
61
- return `${declaration} = ${format(expression)}`;
62
- }
63
-
64
- function formatFunctionCall(code) {
65
- const [fn, ...args] = code;
66
- let formattedFn = format(fn);
67
- if (formattedFn.includes("/") || formattedFn.includes("(")) {
68
- formattedFn = `(${formattedFn})`;
69
- }
70
- return `${formattedFn}${formatArguments(args)}`;
71
- }
72
-
73
- function formatObject(code) {
74
- const [_, ...entries] = code;
75
- const formatted = entries.map(([key, value]) => {
76
- return value === null ? key : `${key}: ${format(value)}`;
77
- });
78
- return formatted ? `{ ${formatted.join(", ")} }` : "{}";
79
- }
80
-
81
- function formatName(name) {
82
- return typeof name === "string"
83
- ? name
84
- : name instanceof Array
85
- ? `(${format(name)})`
86
- : format(name);
87
- }
88
-
89
- function formatLambda(code) {
90
- // TODO: named parameters
91
- return `=${format(code[2])}`;
92
- }
93
-
94
- function formatScopeTraversal(code, implicitFunctionCall = false) {
95
- const operands = code.slice(1);
96
- const name = formatName(operands[0]);
97
- if (operands.length === 1) {
98
- return implicitFunctionCall ? `${name}()` : name;
99
- }
100
-
101
- const args = formatArguments(operands.slice(1));
102
- return `${name}${args}`;
103
- }
104
-
105
- function formatSlashPath(args) {
106
- return "/" + args.join("/");
107
- }
108
-
109
- function formatTemplate(code) {
110
- const args = code.slice(1);
111
- const formatted = args.map((arg) =>
112
- typeof arg === "string" ? arg : `{{${format(arg)}}}`
113
- );
114
- return `\`${formatted.join("")}\``;
115
- }
116
-
117
- function formatTree(code) {
118
- const [_, ...entries] = code;
119
- const formatted = entries.map(([key, value]) => {
120
- const rhs =
121
- typeof value === "function" && value.code !== undefined
122
- ? value.code
123
- : value;
124
- return `${key} = ${format(rhs)}`;
125
- });
126
- return formatted ? `{ ${formatted.join(", ")} }` : "{}";
127
- }