@weborigami/language 0.0.61 → 0.0.63
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/main.js +5 -8
- package/package.json +3 -3
- package/src/compiler/origami.pegjs +17 -33
- package/src/compiler/parse.js +202 -304
- package/src/runtime/HandleExtensionsTransform.js +2 -9
- package/src/runtime/OrigamiFiles.d.ts +2 -2
- package/src/runtime/OrigamiFiles.js +2 -2
- package/src/runtime/evaluate.js +2 -7
- package/src/runtime/expressionObject.js +103 -0
- package/src/runtime/extensions.js +94 -0
- package/src/runtime/internal.js +0 -4
- package/src/runtime/ops.js +12 -34
- package/test/compiler/compile.test.js +2 -2
- package/test/compiler/parse.test.js +38 -46
- package/test/runtime/expressionObject.test.js +66 -0
- package/test/runtime/extensions.test.js +36 -0
- package/test/runtime/ops.test.js +2 -31
- package/src/runtime/ExpressionTree.js +0 -6
- package/src/runtime/OrigamiTransform.d.ts +0 -5
- package/src/runtime/OrigamiTransform.js +0 -3
- package/src/runtime/OrigamiTree.js +0 -4
- package/src/runtime/extname.js +0 -20
- package/src/runtime/handleExtension.js +0 -56
- package/test/runtime/ExpressionTree.test.js +0 -27
- package/test/runtime/HandleExtensionsTransform.test.js +0 -40
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import handleExtension from "./handleExtension.js";
|
|
1
|
+
import { attachHandlerIfApplicable } from "./extensions.js";
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
@@ -12,13 +11,7 @@ export default function HandleExtensionsTransform(Base) {
|
|
|
12
11
|
return class FileLoaders extends Base {
|
|
13
12
|
async get(key) {
|
|
14
13
|
let value = await super.get(key);
|
|
15
|
-
|
|
16
|
-
// If the key is string-like and has an extension, attach a loader (if one
|
|
17
|
-
// exists) that handles that extension.
|
|
18
|
-
if (value && isStringLike(key)) {
|
|
19
|
-
value = await handleExtension(this, String(key), value);
|
|
20
|
-
}
|
|
21
|
-
|
|
14
|
+
value = attachHandlerIfApplicable(this, value, key);
|
|
22
15
|
return value;
|
|
23
16
|
}
|
|
24
17
|
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { FileTree } from "@weborigami/async-tree";
|
|
2
2
|
import EventTargetMixin from "./EventTargetMixin.js";
|
|
3
|
+
import HandleExtensionsTransform from "./HandleExtensionsTransform.js";
|
|
3
4
|
import ImportModulesMixin from "./ImportModulesMixin.js";
|
|
4
|
-
import OrigamiTransform from "./OrigamiTransform.js";
|
|
5
5
|
import WatchFilesMixin from "./WatchFilesMixin.js";
|
|
6
6
|
|
|
7
|
-
export default class OrigamiFiles extends
|
|
7
|
+
export default class OrigamiFiles extends HandleExtensionsTransform(
|
|
8
8
|
(
|
|
9
9
|
ImportModulesMixin(WatchFilesMixin(EventTargetMixin(FileTree)))
|
|
10
10
|
)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { FileTree } from "@weborigami/async-tree";
|
|
2
2
|
import EventTargetMixin from "./EventTargetMixin.js";
|
|
3
|
+
import HandleExtensionsTransform from "./HandleExtensionsTransform.js";
|
|
3
4
|
import ImportModulesMixin from "./ImportModulesMixin.js";
|
|
4
|
-
import OrigamiTransform from "./OrigamiTransform.js";
|
|
5
5
|
import WatchFilesMixin from "./WatchFilesMixin.js";
|
|
6
6
|
|
|
7
|
-
export default class OrigamiFiles extends
|
|
7
|
+
export default class OrigamiFiles extends HandleExtensionsTransform(
|
|
8
8
|
ImportModulesMixin(WatchFilesMixin(EventTargetMixin(FileTree)))
|
|
9
9
|
) {}
|
package/src/runtime/evaluate.js
CHANGED
|
@@ -27,7 +27,7 @@ export default async function evaluate(code) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
let evaluated;
|
|
30
|
-
const unevaluatedFns = [ops.lambda, ops.object
|
|
30
|
+
const unevaluatedFns = [ops.lambda, ops.object];
|
|
31
31
|
if (unevaluatedFns.includes(code[0])) {
|
|
32
32
|
// Don't evaluate instructions, use as is.
|
|
33
33
|
evaluated = code;
|
|
@@ -79,12 +79,7 @@ export default async function evaluate(code) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// To aid debugging, add the code to the result.
|
|
82
|
-
if (
|
|
83
|
-
result &&
|
|
84
|
-
typeof result === "object" &&
|
|
85
|
-
Object.isExtensible(result) &&
|
|
86
|
-
!isPlainObject(result)
|
|
87
|
-
) {
|
|
82
|
+
if (Object.isExtensible(result) && !isPlainObject(result)) {
|
|
88
83
|
try {
|
|
89
84
|
result[codeSymbol] = code;
|
|
90
85
|
if (/** @type {any} */ (code).location) {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ObjectTree, symbols } from "@weborigami/async-tree";
|
|
2
|
+
import { attachHandlerIfApplicable, extname } from "./extensions.js";
|
|
3
|
+
import { evaluate, ops } from "./internal.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Given an array of entries with string keys and Origami code values (arrays of
|
|
7
|
+
* ops and operands), return an object with the same keys defining properties
|
|
8
|
+
* whose getters evaluate the code.
|
|
9
|
+
*
|
|
10
|
+
* The value can take three forms:
|
|
11
|
+
*
|
|
12
|
+
* 1. A primitive value (string, etc.). This will be defined directly as an
|
|
13
|
+
* object property.
|
|
14
|
+
* 1. An immediate code entry. This will be evaluated during this call and its
|
|
15
|
+
* result defined as an object property.
|
|
16
|
+
* 1. A code entry that starts with ops.getter. This will be defined as a
|
|
17
|
+
* property getter on the object.
|
|
18
|
+
*
|
|
19
|
+
* @param {*} entries
|
|
20
|
+
* @param {import("@weborigami/types").AsyncTree | null} parent
|
|
21
|
+
*/
|
|
22
|
+
export default async function expressionObject(entries, parent) {
|
|
23
|
+
// Create the object and set its parent
|
|
24
|
+
const object = {};
|
|
25
|
+
Object.defineProperty(object, symbols.parent, {
|
|
26
|
+
value: parent,
|
|
27
|
+
writable: true,
|
|
28
|
+
configurable: true,
|
|
29
|
+
enumerable: false,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
let tree;
|
|
33
|
+
const immediateProperties = [];
|
|
34
|
+
for (let [key, value] of entries) {
|
|
35
|
+
// Determine if we need to define a getter or a regular property. If the key
|
|
36
|
+
// has an extension, we need to define a getter. If the value is code (an
|
|
37
|
+
// array), we need to define a getter -- but if that code takes the form
|
|
38
|
+
// [ops.getter, <primitive>], we can define a regular property.
|
|
39
|
+
|
|
40
|
+
let defineProperty;
|
|
41
|
+
const extension = extname(key);
|
|
42
|
+
if (extension) {
|
|
43
|
+
defineProperty = false;
|
|
44
|
+
} else if (!(value instanceof Array)) {
|
|
45
|
+
defineProperty = true;
|
|
46
|
+
} else if (value[0] === ops.getter && !(value[1] instanceof Array)) {
|
|
47
|
+
defineProperty = true;
|
|
48
|
+
value = value[1];
|
|
49
|
+
} else {
|
|
50
|
+
defineProperty = false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (defineProperty) {
|
|
54
|
+
// Define simple property
|
|
55
|
+
object[key] = value;
|
|
56
|
+
} else {
|
|
57
|
+
// Property getter
|
|
58
|
+
let code;
|
|
59
|
+
if (value[0] === ops.getter) {
|
|
60
|
+
code = value[1];
|
|
61
|
+
} else {
|
|
62
|
+
immediateProperties.push(key);
|
|
63
|
+
code = value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let get;
|
|
67
|
+
if (extension) {
|
|
68
|
+
// Key has extension, getter will invoke code then attach unpack method
|
|
69
|
+
get = async () => {
|
|
70
|
+
tree ??= new ObjectTree(object);
|
|
71
|
+
const result = await evaluate.call(tree, code);
|
|
72
|
+
return attachHandlerIfApplicable(tree, result, key);
|
|
73
|
+
};
|
|
74
|
+
} else {
|
|
75
|
+
// No extension, so getter just invokes code.
|
|
76
|
+
get = async () => {
|
|
77
|
+
tree ??= new ObjectTree(object);
|
|
78
|
+
return evaluate.call(tree, code);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
Object.defineProperty(object, key, {
|
|
83
|
+
configurable: true,
|
|
84
|
+
enumerable: true,
|
|
85
|
+
get,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Evaluate any properties that were declared as immediate: get their value
|
|
91
|
+
// and overwrite the property getter with the actual value.
|
|
92
|
+
for (const key of immediateProperties) {
|
|
93
|
+
const value = await object[key];
|
|
94
|
+
Object.defineProperty(object, key, {
|
|
95
|
+
configurable: true,
|
|
96
|
+
enumerable: true,
|
|
97
|
+
value,
|
|
98
|
+
writable: true,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return object;
|
|
103
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
box,
|
|
3
|
+
isPacked,
|
|
4
|
+
isStringLike,
|
|
5
|
+
isUnpackable,
|
|
6
|
+
scope,
|
|
7
|
+
symbols,
|
|
8
|
+
toString,
|
|
9
|
+
} from "@weborigami/async-tree";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* If the given value is packed (e.g., buffer) and the key is a string-like path
|
|
13
|
+
* that ends in an extension, search for a handler for that extension and, if
|
|
14
|
+
* found, attach it to the value.
|
|
15
|
+
*
|
|
16
|
+
* @param {import("@weborigami/types").AsyncTree} parent
|
|
17
|
+
* @param {any} value
|
|
18
|
+
* @param {any} key
|
|
19
|
+
*/
|
|
20
|
+
export async function attachHandlerIfApplicable(parent, value, key) {
|
|
21
|
+
if (isPacked(value) && isStringLike(key)) {
|
|
22
|
+
key = toString(key);
|
|
23
|
+
|
|
24
|
+
// Special case: `.ori.<ext>` extensions are Origami documents.
|
|
25
|
+
const extension = key.match(/\.ori\.\S+$/) ? ".ori_document" : extname(key);
|
|
26
|
+
if (extension) {
|
|
27
|
+
const handler = await getExtensionHandler(parent, extension);
|
|
28
|
+
if (handler) {
|
|
29
|
+
// If the value is a primitive, box it so we can attach data to it.
|
|
30
|
+
value = box(value);
|
|
31
|
+
|
|
32
|
+
if (handler.mediaType) {
|
|
33
|
+
value.mediaType = handler.mediaType;
|
|
34
|
+
}
|
|
35
|
+
value[symbols.parent] = parent;
|
|
36
|
+
|
|
37
|
+
const unpack = handler.unpack;
|
|
38
|
+
if (unpack) {
|
|
39
|
+
// Wrap the unpack function so its only called once per value.
|
|
40
|
+
let loaded;
|
|
41
|
+
value.unpack = async () => {
|
|
42
|
+
loaded ??= await unpack(value, { key, parent });
|
|
43
|
+
return loaded;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Find an extension handler for a file in the given container.
|
|
54
|
+
*
|
|
55
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
56
|
+
*
|
|
57
|
+
* @param {AsyncTree} parent
|
|
58
|
+
* @param {string} extension
|
|
59
|
+
*/
|
|
60
|
+
export async function getExtensionHandler(parent, extension) {
|
|
61
|
+
const handlerName = `${extension.slice(1)}_handler`;
|
|
62
|
+
const parentScope = scope(parent);
|
|
63
|
+
/** @type {import("../../index.ts").ExtensionHandler} */
|
|
64
|
+
let extensionHandler = await parentScope?.get(handlerName);
|
|
65
|
+
if (isUnpackable(extensionHandler)) {
|
|
66
|
+
// The extension handler itself needs to be unpacked. E.g., if it's a
|
|
67
|
+
// buffer containing JavaScript file, we need to unpack it to get its
|
|
68
|
+
// default export.
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
extensionHandler = await extensionHandler.unpack();
|
|
71
|
+
}
|
|
72
|
+
return extensionHandler;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* If the given path ends in an extension, return it. Otherwise, return the
|
|
77
|
+
* empty string.
|
|
78
|
+
*
|
|
79
|
+
* This is meant as a basic replacement for the standard Node `path.extname`.
|
|
80
|
+
* That standard function inaccurately returns an extension for a path that
|
|
81
|
+
* includes a near-final extension but ends in a final slash, like `foo.txt/`.
|
|
82
|
+
* Node thinks that path has a ".txt" extension, but for our purposes it
|
|
83
|
+
* doesn't.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} path
|
|
86
|
+
*/
|
|
87
|
+
export function extname(path) {
|
|
88
|
+
// We want at least one character before the dot, then a dot, then a non-empty
|
|
89
|
+
// sequence of characters after the dot that aren't slahes or dots.
|
|
90
|
+
const extnameRegex = /[^/](?<ext>\.[^/\.]+)$/;
|
|
91
|
+
const match = String(path).match(extnameRegex);
|
|
92
|
+
const extension = match?.groups?.ext.toLowerCase() ?? "";
|
|
93
|
+
return extension;
|
|
94
|
+
}
|
package/src/runtime/internal.js
CHANGED
|
@@ -12,7 +12,3 @@ export * as ops from "./ops.js";
|
|
|
12
12
|
export { default as evaluate } from "./evaluate.js";
|
|
13
13
|
|
|
14
14
|
export * as expressionFunction from "./expressionFunction.js";
|
|
15
|
-
|
|
16
|
-
export { default as ExpressionTree } from "./ExpressionTree.js";
|
|
17
|
-
|
|
18
|
-
export { default as OrigamiTree } from "./OrigamiTree.js";
|
package/src/runtime/ops.js
CHANGED
|
@@ -11,11 +11,12 @@ import {
|
|
|
11
11
|
scope as scopeFn,
|
|
12
12
|
concat as treeConcat,
|
|
13
13
|
} from "@weborigami/async-tree";
|
|
14
|
+
import expressionObject from "./expressionObject.js";
|
|
15
|
+
import { attachHandlerIfApplicable } from "./extensions.js";
|
|
14
16
|
import HandleExtensionsTransform from "./HandleExtensionsTransform.js";
|
|
15
|
-
import
|
|
16
|
-
import handleExtension from "./handleExtension.js";
|
|
17
|
-
import { OrigamiTree, evaluate, expressionFunction } from "./internal.js";
|
|
17
|
+
import { evaluate } from "./internal.js";
|
|
18
18
|
import mergeTrees from "./mergeTrees.js";
|
|
19
|
+
import OrigamiFiles from "./OrigamiFiles.js";
|
|
19
20
|
|
|
20
21
|
// For memoizing lambda functions
|
|
21
22
|
const lambdaFnMap = new Map();
|
|
@@ -104,12 +105,18 @@ async function fetchResponse(href) {
|
|
|
104
105
|
const url = new URL(href);
|
|
105
106
|
const filename = url.pathname.split("/").pop();
|
|
106
107
|
if (this && filename) {
|
|
107
|
-
buffer = await
|
|
108
|
+
buffer = await attachHandlerIfApplicable(this, buffer, filename);
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
return buffer;
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
/**
|
|
115
|
+
* This op is only used during parsing. It signals to ops.object that the
|
|
116
|
+
* "arguments" of the expression should be used to define a property getter.
|
|
117
|
+
*/
|
|
118
|
+
export const getter = new String("«ops.getter»");
|
|
119
|
+
|
|
113
120
|
/**
|
|
114
121
|
* Construct a files tree for the filesystem root.
|
|
115
122
|
*
|
|
@@ -246,13 +253,7 @@ merge.toString = () => "«ops.merge»";
|
|
|
246
253
|
* @param {any[]} entries
|
|
247
254
|
*/
|
|
248
255
|
export async function object(...entries) {
|
|
249
|
-
|
|
250
|
-
const promises = entries.map(async ([key, value]) => [
|
|
251
|
-
key,
|
|
252
|
-
await evaluate.call(tree, value),
|
|
253
|
-
]);
|
|
254
|
-
const evaluated = await Promise.all(promises);
|
|
255
|
-
return Object.fromEntries(evaluated);
|
|
256
|
+
return expressionObject(entries, this);
|
|
256
257
|
}
|
|
257
258
|
object.toString = () => "«ops.object»";
|
|
258
259
|
|
|
@@ -286,29 +287,6 @@ spread.toString = () => "«ops.spread»";
|
|
|
286
287
|
*/
|
|
287
288
|
export const traverse = Tree.traverseOrThrow;
|
|
288
289
|
|
|
289
|
-
/**
|
|
290
|
-
* Construct an tree. This is similar to ops.object but the values are turned
|
|
291
|
-
* into functions rather than being immediately evaluated, and the result is an
|
|
292
|
-
* OrigamiTree.
|
|
293
|
-
*
|
|
294
|
-
* @this {AsyncTree|null}
|
|
295
|
-
* @param {any[]} entries
|
|
296
|
-
*/
|
|
297
|
-
export async function tree(...entries) {
|
|
298
|
-
const fns = entries.map(([key, code]) => {
|
|
299
|
-
const value =
|
|
300
|
-
code instanceof Array
|
|
301
|
-
? expressionFunction.createExpressionFunction(code)
|
|
302
|
-
: code;
|
|
303
|
-
return [key, value];
|
|
304
|
-
});
|
|
305
|
-
const object = Object.fromEntries(fns);
|
|
306
|
-
const result = new OrigamiTree(object);
|
|
307
|
-
result.parent = this;
|
|
308
|
-
return result;
|
|
309
|
-
}
|
|
310
|
-
tree.toString = () => "«ops.tree»";
|
|
311
|
-
|
|
312
290
|
/**
|
|
313
291
|
* A website tree via HTTP.
|
|
314
292
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ObjectTree, Tree } from "@weborigami/async-tree";
|
|
1
|
+
import { ObjectTree, symbols, Tree } from "@weborigami/async-tree";
|
|
2
2
|
import assert from "node:assert";
|
|
3
3
|
import { describe, test } from "node:test";
|
|
4
4
|
import * as compile from "../../src/compiler/compile.js";
|
|
@@ -24,7 +24,7 @@ describe("compile", () => {
|
|
|
24
24
|
test("tree", async () => {
|
|
25
25
|
const fn = compile.expression("{ message = greet(name) }");
|
|
26
26
|
const tree = await fn.call(null);
|
|
27
|
-
tree.parent = shared;
|
|
27
|
+
tree[symbols.parent] = shared;
|
|
28
28
|
assert.deepEqual(await Tree.plain(tree), {
|
|
29
29
|
message: "Hello, Alice!",
|
|
30
30
|
});
|
|
@@ -24,22 +24,6 @@ describe("Origami parser", () => {
|
|
|
24
24
|
]);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
test("treeAssignment", () => {
|
|
28
|
-
assertParse("treeAssignment", "data = obj.json", [
|
|
29
|
-
"data",
|
|
30
|
-
[ops.scope, "obj.json"],
|
|
31
|
-
]);
|
|
32
|
-
assertParse("treeAssignment", "foo = fn 'bar'", [
|
|
33
|
-
"foo",
|
|
34
|
-
[[ops.scope, "fn"], "bar"],
|
|
35
|
-
]);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("treeEntry", () => {
|
|
39
|
-
assertParse("treeEntry", "foo", ["foo", [ops.inherited, "foo"]]);
|
|
40
|
-
assertParse("treeEntry", "foo = 1", ["foo", 1]);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
27
|
test("expr", () => {
|
|
44
28
|
assertParse("expr", "obj.json", [ops.scope, "obj.json"]);
|
|
45
29
|
assertParse("expr", "(fn a, b, c)", [
|
|
@@ -93,20 +77,26 @@ describe("Origami parser", () => {
|
|
|
93
77
|
}
|
|
94
78
|
`,
|
|
95
79
|
[
|
|
96
|
-
ops.
|
|
80
|
+
ops.object,
|
|
97
81
|
[
|
|
98
82
|
"index.html",
|
|
99
83
|
[
|
|
100
|
-
|
|
101
|
-
[
|
|
84
|
+
ops.getter,
|
|
85
|
+
[
|
|
86
|
+
[ops.scope, "index.ori"],
|
|
87
|
+
[ops.scope, "teamData.yaml"],
|
|
88
|
+
],
|
|
102
89
|
],
|
|
103
90
|
],
|
|
104
91
|
[
|
|
105
92
|
"thumbnails",
|
|
106
93
|
[
|
|
107
|
-
|
|
108
|
-
[
|
|
109
|
-
|
|
94
|
+
ops.getter,
|
|
95
|
+
[
|
|
96
|
+
[ops.scope, "@map"],
|
|
97
|
+
[ops.scope, "images"],
|
|
98
|
+
[ops.object, ["value", [ops.scope, "thumbnail.js"]]],
|
|
99
|
+
],
|
|
110
100
|
],
|
|
111
101
|
],
|
|
112
102
|
]
|
|
@@ -297,13 +287,22 @@ describe("Origami parser", () => {
|
|
|
297
287
|
assertParse("object", "{ a: 1, b }", [
|
|
298
288
|
ops.object,
|
|
299
289
|
["a", 1],
|
|
300
|
-
["b", [ops.
|
|
290
|
+
["b", [ops.inherited, "b"]],
|
|
301
291
|
]);
|
|
302
292
|
assertParse("object", `{ "a": 1, "b": 2 }`, [
|
|
303
293
|
ops.object,
|
|
304
294
|
["a", 1],
|
|
305
295
|
["b", 2],
|
|
306
296
|
]);
|
|
297
|
+
assertParse("object", "{ a = b, b: 2 }", [
|
|
298
|
+
ops.object,
|
|
299
|
+
["a", [ops.getter, [ops.scope, "b"]]],
|
|
300
|
+
["b", 2],
|
|
301
|
+
]);
|
|
302
|
+
assertParse("object", "{ x = fn('a') }", [
|
|
303
|
+
ops.object,
|
|
304
|
+
["x", [ops.getter, [[ops.scope, "fn"], "a"]]],
|
|
305
|
+
]);
|
|
307
306
|
assertParse("object", "{ a: 1, ...b }", [
|
|
308
307
|
ops.merge,
|
|
309
308
|
[ops.object, ["a", 1]],
|
|
@@ -311,6 +310,22 @@ describe("Origami parser", () => {
|
|
|
311
310
|
]);
|
|
312
311
|
});
|
|
313
312
|
|
|
313
|
+
test("objectEntry", () => {
|
|
314
|
+
assertParse("objectEntry", "foo", ["foo", [ops.inherited, "foo"]]);
|
|
315
|
+
assertParse("objectEntry", "x: y", ["x", [ops.scope, "y"]]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("objectGetter", () => {
|
|
319
|
+
assertParse("objectGetter", "data = obj.json", [
|
|
320
|
+
"data",
|
|
321
|
+
[ops.getter, [ops.scope, "obj.json"]],
|
|
322
|
+
]);
|
|
323
|
+
assertParse("objectGetter", "foo = fn 'bar'", [
|
|
324
|
+
"foo",
|
|
325
|
+
[ops.getter, [[ops.scope, "fn"], "bar"]],
|
|
326
|
+
]);
|
|
327
|
+
});
|
|
328
|
+
|
|
314
329
|
test("objectProperty", () => {
|
|
315
330
|
assertParse("objectProperty", "a: 1", ["a", 1]);
|
|
316
331
|
assertParse("objectProperty", "name: 'Alice'", ["name", "Alice"]);
|
|
@@ -320,11 +335,6 @@ describe("Origami parser", () => {
|
|
|
320
335
|
]);
|
|
321
336
|
});
|
|
322
337
|
|
|
323
|
-
test("objectEntry", () => {
|
|
324
|
-
assertParse("objectEntry", "foo", ["foo", [ops.scope, "foo"]]);
|
|
325
|
-
assertParse("objectEntry", "x: y", ["x", [ops.scope, "y"]]);
|
|
326
|
-
});
|
|
327
|
-
|
|
328
338
|
test("parameterizedLambda", () => {
|
|
329
339
|
assertParse("parameterizedLambda", "() => foo", [
|
|
330
340
|
ops.lambda,
|
|
@@ -496,24 +506,6 @@ describe("Origami parser", () => {
|
|
|
496
506
|
assertParse("templateSubstitution", "${foo}", [ops.scope, "foo"]);
|
|
497
507
|
});
|
|
498
508
|
|
|
499
|
-
test("tree", () => {
|
|
500
|
-
assertParse("tree", "{}", [ops.tree]);
|
|
501
|
-
assertParse("tree", "{ a = 1, b }", [
|
|
502
|
-
ops.tree,
|
|
503
|
-
["a", 1],
|
|
504
|
-
["b", [ops.inherited, "b"]],
|
|
505
|
-
]);
|
|
506
|
-
assertParse("tree", "{ x = fn('a') }", [
|
|
507
|
-
ops.tree,
|
|
508
|
-
["x", [[ops.scope, "fn"], "a"]],
|
|
509
|
-
]);
|
|
510
|
-
assertParse("tree", "{ a = 1, ...b }", [
|
|
511
|
-
ops.merge,
|
|
512
|
-
[ops.tree, ["a", 1]],
|
|
513
|
-
[ops.scope, "b"],
|
|
514
|
-
]);
|
|
515
|
-
});
|
|
516
|
-
|
|
517
509
|
test("whitespace block", () => {
|
|
518
510
|
assertParse(
|
|
519
511
|
"__",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ObjectTree, symbols, Tree } from "@weborigami/async-tree";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { describe, test } from "node:test";
|
|
4
|
+
|
|
5
|
+
import expressionObject from "../../src/runtime/expressionObject.js";
|
|
6
|
+
import { ops } from "../../src/runtime/internal.js";
|
|
7
|
+
|
|
8
|
+
describe("expressionObject", () => {
|
|
9
|
+
test("can instantiate an object", async () => {
|
|
10
|
+
const scope = new ObjectTree({
|
|
11
|
+
upper: (s) => s.toUpperCase(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const entries = [
|
|
15
|
+
["hello", [[ops.scope, "upper"], "hello"]],
|
|
16
|
+
["world", [[ops.scope, "upper"], "world"]],
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const object = await expressionObject(entries, scope);
|
|
20
|
+
assert.equal(await object.hello, "HELLO");
|
|
21
|
+
assert.equal(await object.world, "WORLD");
|
|
22
|
+
assert.equal(object[symbols.parent], scope);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("can define a property getter", async () => {
|
|
26
|
+
let count = 0;
|
|
27
|
+
const increment = () => count++;
|
|
28
|
+
const entries = [["count", [ops.getter, [increment]]]];
|
|
29
|
+
const object = await expressionObject(entries, null);
|
|
30
|
+
assert.equal(await object.count, 0);
|
|
31
|
+
assert.equal(await object.count, 1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("treats a getter for a primitive value as a regular property", async () => {
|
|
35
|
+
const entries = [["name", [ops.getter, "world"]]];
|
|
36
|
+
const object = await expressionObject(entries, null);
|
|
37
|
+
assert.equal(object.name, "world");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("can instantiate an Origami tree", async () => {
|
|
41
|
+
const entries = [
|
|
42
|
+
["name", "world"],
|
|
43
|
+
["message", [ops.concat, "Hello, ", [ops.scope, "name"], "!"]],
|
|
44
|
+
];
|
|
45
|
+
const parent = new ObjectTree({});
|
|
46
|
+
const object = await expressionObject(entries, parent);
|
|
47
|
+
assert.deepEqual(await Tree.plain(object), {
|
|
48
|
+
name: "world",
|
|
49
|
+
message: "Hello, world!",
|
|
50
|
+
});
|
|
51
|
+
assert.equal(object[symbols.parent], parent);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("returned object values can be unpacked", async () => {
|
|
55
|
+
const entries = [["data.json", `{ "a": 1 }`]];
|
|
56
|
+
const parent = new ObjectTree({
|
|
57
|
+
json_handler: {
|
|
58
|
+
unpack: JSON.parse,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
const result = await expressionObject(entries, parent);
|
|
62
|
+
const dataJson = await result["data.json"];
|
|
63
|
+
const json = await dataJson.unpack();
|
|
64
|
+
assert.deepEqual(json, { a: 1 });
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ObjectTree } from "@weborigami/async-tree";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { describe, test } from "node:test";
|
|
4
|
+
import { attachHandlerIfApplicable } from "../../src/runtime/extensions.js";
|
|
5
|
+
|
|
6
|
+
describe("extensions", () => {
|
|
7
|
+
test("attaches an unpack method to a value with an extension", async () => {
|
|
8
|
+
const fixture = createFixture();
|
|
9
|
+
const numberValue = await fixture.get("foo");
|
|
10
|
+
assert(typeof numberValue === "number");
|
|
11
|
+
assert.equal(numberValue, 1);
|
|
12
|
+
const jsonFile = await fixture.get("bar.json");
|
|
13
|
+
const withHandler = await attachHandlerIfApplicable(
|
|
14
|
+
fixture,
|
|
15
|
+
jsonFile,
|
|
16
|
+
"bar.json"
|
|
17
|
+
);
|
|
18
|
+
assert.equal(String(withHandler), `{ "bar": 2 }`);
|
|
19
|
+
const data = await withHandler.unpack();
|
|
20
|
+
assert.deepEqual(data, { bar: 2 });
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function createFixture() {
|
|
25
|
+
const parent = new ObjectTree({
|
|
26
|
+
json_handler: {
|
|
27
|
+
unpack: (buffer) => JSON.parse(String(buffer)),
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
let tree = new ObjectTree({
|
|
31
|
+
foo: 1, // No extension, should be left alone
|
|
32
|
+
"bar.json": `{ "bar": 2 }`,
|
|
33
|
+
});
|
|
34
|
+
tree.parent = parent;
|
|
35
|
+
return tree;
|
|
36
|
+
}
|
package/test/runtime/ops.test.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import { ObjectTree
|
|
1
|
+
import { ObjectTree } from "@weborigami/async-tree";
|
|
2
2
|
import assert from "node:assert";
|
|
3
3
|
import { describe, test } from "node:test";
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
OrigamiTree,
|
|
7
|
-
evaluate,
|
|
8
|
-
expressionFunction,
|
|
9
|
-
ops,
|
|
10
|
-
} from "../../src/runtime/internal.js";
|
|
5
|
+
import { evaluate, ops } from "../../src/runtime/internal.js";
|
|
11
6
|
|
|
12
7
|
describe("ops", () => {
|
|
13
8
|
test("can resolve substitutions in a template literal", async () => {
|
|
@@ -85,30 +80,6 @@ describe("ops", () => {
|
|
|
85
80
|
assert.deepEqual(result, ["Hello", 1, "WORLD"]);
|
|
86
81
|
});
|
|
87
82
|
|
|
88
|
-
test("can instantiate an Origami tree", async () => {
|
|
89
|
-
const code = [
|
|
90
|
-
ops.tree,
|
|
91
|
-
["name", "world"],
|
|
92
|
-
[
|
|
93
|
-
"message",
|
|
94
|
-
expressionFunction.createExpressionFunction([
|
|
95
|
-
ops.concat,
|
|
96
|
-
"Hello, ",
|
|
97
|
-
[ops.scope, "name"],
|
|
98
|
-
"!",
|
|
99
|
-
]),
|
|
100
|
-
],
|
|
101
|
-
];
|
|
102
|
-
const parent = new ObjectTree({});
|
|
103
|
-
const result = await evaluate.call(parent, code);
|
|
104
|
-
assert(result instanceof OrigamiTree);
|
|
105
|
-
assert.deepEqual(await Tree.plain(result), {
|
|
106
|
-
name: "world",
|
|
107
|
-
message: "Hello, world!",
|
|
108
|
-
});
|
|
109
|
-
assert.equal(result.parent, parent);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
83
|
test("can search inherited scope", async () => {
|
|
113
84
|
const parent = new ObjectTree({
|
|
114
85
|
a: 1, // This is the inherited value we want
|