@weborigami/language 0.0.66-beta.1 → 0.0.66-beta.2
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 +5 -5
- package/src/compiler/origami.pegjs +40 -22
- package/src/compiler/parse.js +282 -204
- package/src/compiler/parserHelpers.js +39 -4
- package/src/runtime/HandleExtensionsTransform.js +3 -4
- package/src/runtime/ImportModulesMixin.js +13 -10
- package/src/runtime/InvokeFunctionsTransform.js +0 -6
- package/src/runtime/expressionObject.js +3 -3
- package/src/runtime/extensions.js +55 -47
- package/src/runtime/formatError.js +5 -1
- package/src/runtime/ops.js +71 -28
- package/test/compiler/compile.test.js +7 -1
- package/test/compiler/parse.test.js +72 -29
- package/test/runtime/extensions.test.js +9 -6
- package/test/runtime/ops.test.js +38 -31
|
@@ -13,6 +13,22 @@ export function annotate(parseResult, location) {
|
|
|
13
13
|
return parseResult;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
// Return true if the code will generate an async object.
|
|
17
|
+
function isAsyncObject(code) {
|
|
18
|
+
if (!(code instanceof Array)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (code[0] !== ops.object) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
// Are any of the properties getters?
|
|
25
|
+
const entries = code.slice(1);
|
|
26
|
+
const hasGetter = entries.some(([key, value]) => {
|
|
27
|
+
return value instanceof Array && value[0] === ops.getter;
|
|
28
|
+
});
|
|
29
|
+
return hasGetter;
|
|
30
|
+
}
|
|
31
|
+
|
|
16
32
|
export function makeArray(entries) {
|
|
17
33
|
let currentEntries = [];
|
|
18
34
|
const spreads = [];
|
|
@@ -72,9 +88,14 @@ export function makeFunctionCall(target, chain, location) {
|
|
|
72
88
|
|
|
73
89
|
// @ts-ignore
|
|
74
90
|
fnCall =
|
|
75
|
-
args[0]
|
|
76
|
-
?
|
|
77
|
-
|
|
91
|
+
args[0] !== ops.traverse
|
|
92
|
+
? // Function call
|
|
93
|
+
[value, ...args]
|
|
94
|
+
: args.length > 1
|
|
95
|
+
? // Traverse
|
|
96
|
+
[ops.traverse, value, ...args.slice(1)]
|
|
97
|
+
: // Traverse without arguments equates to unpack
|
|
98
|
+
[ops.unpack, value];
|
|
78
99
|
|
|
79
100
|
// Create a location spanning the newly-constructed function call.
|
|
80
101
|
if (args instanceof Array) {
|
|
@@ -97,14 +118,28 @@ export function makeObject(entries, op) {
|
|
|
97
118
|
let currentEntries = [];
|
|
98
119
|
const spreads = [];
|
|
99
120
|
|
|
100
|
-
for (
|
|
121
|
+
for (let [key, value] of entries) {
|
|
101
122
|
if (key === ops.spread) {
|
|
123
|
+
// Accumulate spread entry
|
|
102
124
|
if (currentEntries.length > 0) {
|
|
103
125
|
spreads.push([op, ...currentEntries]);
|
|
104
126
|
currentEntries = [];
|
|
105
127
|
}
|
|
106
128
|
spreads.push(value);
|
|
107
129
|
} else {
|
|
130
|
+
if (
|
|
131
|
+
value instanceof Array &&
|
|
132
|
+
value[0] === ops.getter &&
|
|
133
|
+
value[1] instanceof Array &&
|
|
134
|
+
value[1][0] === ops.primitive
|
|
135
|
+
) {
|
|
136
|
+
// Simplify a getter for a primitive value to a regular property
|
|
137
|
+
value = value[1];
|
|
138
|
+
} else if (isAsyncObject(value)) {
|
|
139
|
+
// Add a trailing slash to key if value is an async object
|
|
140
|
+
key = key + "/";
|
|
141
|
+
}
|
|
142
|
+
|
|
108
143
|
currentEntries.push([key, value]);
|
|
109
144
|
}
|
|
110
145
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { handleExtension } from "./extensions.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
@@ -10,9 +10,8 @@ import { attachHandlerIfApplicable } from "./extensions.js";
|
|
|
10
10
|
export default function HandleExtensionsTransform(Base) {
|
|
11
11
|
return class FileLoaders extends Base {
|
|
12
12
|
async get(key) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return value;
|
|
13
|
+
const value = await super.get(key);
|
|
14
|
+
return handleExtension(this, value, key);
|
|
16
15
|
}
|
|
17
16
|
};
|
|
18
17
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { maybeOrigamiSourceCode } from "./formatError.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
@@ -25,21 +26,23 @@ export default function ImportModulesMixin(Base) {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
// Does the module exist as a file?
|
|
28
|
-
let stats;
|
|
29
29
|
try {
|
|
30
|
-
|
|
30
|
+
await fs.stat(filePath);
|
|
31
31
|
} catch (error) {
|
|
32
|
-
//
|
|
32
|
+
// File doesn't exist
|
|
33
|
+
return undefined;
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
|
|
36
|
+
// Module exists, but we can't load it. Is the error internal?
|
|
37
|
+
if (maybeOrigamiSourceCode(error.message)) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Internal Origami error loading ${filePath}\n${error.message}`
|
|
40
|
+
);
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
+
// Error may be a syntax error, so we offer that as a hint.
|
|
44
|
+
const message = `Error loading ${filePath}, possibly due to a syntax error.\n${error.message}`;
|
|
45
|
+
throw new SyntaxError(message);
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
if ("default" in obj) {
|
|
@@ -21,11 +21,5 @@ export default function InvokeFunctionsTransform(Base) {
|
|
|
21
21
|
}
|
|
22
22
|
return value;
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
// Need to evaluate the value before checking if it is a tree.
|
|
26
|
-
async isKeyForSubtree(key) {
|
|
27
|
-
const value = await this.get(key);
|
|
28
|
-
return Tree.isAsyncTree(value);
|
|
29
|
-
}
|
|
30
24
|
};
|
|
31
25
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ObjectTree, symbols } from "@weborigami/async-tree";
|
|
2
|
-
import {
|
|
2
|
+
import { extname, handleExtension } from "./extensions.js";
|
|
3
3
|
import { evaluate, ops } from "./internal.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -36,7 +36,6 @@ export default async function expressionObject(entries, parent) {
|
|
|
36
36
|
// has an extension, we need to define a getter. If the value is code (an
|
|
37
37
|
// array), we need to define a getter -- but if that code takes the form
|
|
38
38
|
// [ops.getter, <primitive>], we can define a regular property.
|
|
39
|
-
|
|
40
39
|
let defineProperty;
|
|
41
40
|
const extension = extname(key);
|
|
42
41
|
if (extension) {
|
|
@@ -50,6 +49,7 @@ export default async function expressionObject(entries, parent) {
|
|
|
50
49
|
defineProperty = false;
|
|
51
50
|
}
|
|
52
51
|
|
|
52
|
+
// If the key is wrapped in parentheses, it is not enumerable.
|
|
53
53
|
let enumerable = true;
|
|
54
54
|
if (key[0] === "(" && key[key.length - 1] === ")") {
|
|
55
55
|
key = key.slice(1, -1);
|
|
@@ -81,7 +81,7 @@ export default async function expressionObject(entries, parent) {
|
|
|
81
81
|
get = async () => {
|
|
82
82
|
tree ??= new ObjectTree(object);
|
|
83
83
|
const result = await evaluate.call(tree, code);
|
|
84
|
-
return
|
|
84
|
+
return handleExtension(tree, result, key);
|
|
85
85
|
};
|
|
86
86
|
} else {
|
|
87
87
|
// No extension, so getter just invokes code.
|
|
@@ -5,9 +5,53 @@ import {
|
|
|
5
5
|
isUnpackable,
|
|
6
6
|
scope,
|
|
7
7
|
symbols,
|
|
8
|
-
|
|
8
|
+
trailingSlash,
|
|
9
9
|
} from "@weborigami/async-tree";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* If the given path ends in an extension, return it. Otherwise, return the
|
|
13
|
+
* empty string.
|
|
14
|
+
*
|
|
15
|
+
* This is meant as a basic replacement for the standard Node `path.extname`.
|
|
16
|
+
* That standard function inaccurately returns an extension for a path that
|
|
17
|
+
* includes a near-final extension but ends in a final slash, like `foo.txt/`.
|
|
18
|
+
* Node thinks that path has a ".txt" extension, but for our purposes it
|
|
19
|
+
* doesn't.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} path
|
|
22
|
+
*/
|
|
23
|
+
export function extname(path) {
|
|
24
|
+
// We want at least one character before the dot, then a dot, then a non-empty
|
|
25
|
+
// sequence of characters after the dot that aren't slahes or dots.
|
|
26
|
+
const extnameRegex = /[^/](?<ext>\.[^/\.]+)$/;
|
|
27
|
+
const match = String(path).match(extnameRegex);
|
|
28
|
+
const extension = match?.groups?.ext.toLowerCase() ?? "";
|
|
29
|
+
return extension;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Find an extension handler for a file in the given container.
|
|
34
|
+
*
|
|
35
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
36
|
+
*
|
|
37
|
+
* @param {AsyncTree} parent
|
|
38
|
+
* @param {string} extension
|
|
39
|
+
*/
|
|
40
|
+
export async function getExtensionHandler(parent, extension) {
|
|
41
|
+
const handlerName = `${extension.slice(1)}_handler`;
|
|
42
|
+
const parentScope = scope(parent);
|
|
43
|
+
/** @type {import("../../index.ts").ExtensionHandler} */
|
|
44
|
+
let extensionHandler = await parentScope?.get(handlerName);
|
|
45
|
+
if (isUnpackable(extensionHandler)) {
|
|
46
|
+
// The extension handler itself needs to be unpacked. E.g., if it's a
|
|
47
|
+
// buffer containing JavaScript file, we need to unpack it to get its
|
|
48
|
+
// default export.
|
|
49
|
+
// @ts-ignore
|
|
50
|
+
extensionHandler = await extensionHandler.unpack();
|
|
51
|
+
}
|
|
52
|
+
return extensionHandler;
|
|
53
|
+
}
|
|
54
|
+
|
|
11
55
|
/**
|
|
12
56
|
* If the given value is packed (e.g., buffer) and the key is a string-like path
|
|
13
57
|
* that ends in an extension, search for a handler for that extension and, if
|
|
@@ -17,15 +61,23 @@ import {
|
|
|
17
61
|
* @param {any} value
|
|
18
62
|
* @param {any} key
|
|
19
63
|
*/
|
|
20
|
-
export async function
|
|
64
|
+
export async function handleExtension(parent, value, key) {
|
|
21
65
|
if (isPacked(value) && isStringLike(key)) {
|
|
22
|
-
|
|
66
|
+
const hasSlash = trailingSlash.has(key);
|
|
67
|
+
if (hasSlash) {
|
|
68
|
+
key = trailingSlash.remove(key);
|
|
69
|
+
}
|
|
23
70
|
|
|
24
71
|
// Special case: `.ori.<ext>` extensions are Origami documents.
|
|
25
72
|
const extension = key.match(/\.ori\.\S+$/) ? ".ori_document" : extname(key);
|
|
26
73
|
if (extension) {
|
|
27
74
|
const handler = await getExtensionHandler(parent, extension);
|
|
28
75
|
if (handler) {
|
|
76
|
+
if (hasSlash && handler.unpack) {
|
|
77
|
+
// Key like `data.json/` ends in slash -- unpack immediately
|
|
78
|
+
return handler.unpack(value, { key, parent });
|
|
79
|
+
}
|
|
80
|
+
|
|
29
81
|
// If the value is a primitive, box it so we can attach data to it.
|
|
30
82
|
value = box(value);
|
|
31
83
|
|
|
@@ -48,47 +100,3 @@ export async function attachHandlerIfApplicable(parent, value, key) {
|
|
|
48
100
|
}
|
|
49
101
|
return value;
|
|
50
102
|
}
|
|
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
|
-
}
|
|
@@ -23,7 +23,7 @@ export default function formatError(error) {
|
|
|
23
23
|
let lines = error.stack.split("\n");
|
|
24
24
|
for (let i = 0; i < lines.length; i++) {
|
|
25
25
|
const line = lines[i];
|
|
26
|
-
if (
|
|
26
|
+
if (maybeOrigamiSourceCode(line)) {
|
|
27
27
|
break;
|
|
28
28
|
}
|
|
29
29
|
if (message) {
|
|
@@ -50,3 +50,7 @@ export default function formatError(error) {
|
|
|
50
50
|
}
|
|
51
51
|
return message;
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
export function maybeOrigamiSourceCode(text) {
|
|
55
|
+
return origamiSourceSignals.some((signal) => text.includes(signal));
|
|
56
|
+
}
|
package/src/runtime/ops.js
CHANGED
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
ObjectTree,
|
|
8
|
+
OpenSiteTree,
|
|
8
9
|
SiteTree,
|
|
9
10
|
Tree,
|
|
10
11
|
isUnpackable,
|
|
11
12
|
scope as scopeFn,
|
|
13
|
+
trailingSlash,
|
|
12
14
|
concat as treeConcat,
|
|
13
15
|
} from "@weborigami/async-tree";
|
|
14
16
|
import expressionObject from "./expressionObject.js";
|
|
15
|
-
import {
|
|
17
|
+
import { handleExtension } from "./extensions.js";
|
|
16
18
|
import HandleExtensionsTransform from "./HandleExtensionsTransform.js";
|
|
17
19
|
import { evaluate } from "./internal.js";
|
|
18
20
|
import mergeTrees from "./mergeTrees.js";
|
|
@@ -47,24 +49,6 @@ export async function concat(...args) {
|
|
|
47
49
|
}
|
|
48
50
|
concat.toString = () => "«ops.concat»";
|
|
49
51
|
|
|
50
|
-
/**
|
|
51
|
-
* Given a protocol, a host, and a list of keys, construct an href.
|
|
52
|
-
*
|
|
53
|
-
* @param {string} protocol
|
|
54
|
-
* @param {string} host
|
|
55
|
-
* @param {...string|Symbol} keys
|
|
56
|
-
*/
|
|
57
|
-
function constructHref(protocol, host, ...keys) {
|
|
58
|
-
let href = [host, ...keys].join("/");
|
|
59
|
-
if (!href.startsWith(protocol)) {
|
|
60
|
-
if (!href.startsWith("//")) {
|
|
61
|
-
href = `//${href}`;
|
|
62
|
-
}
|
|
63
|
-
href = `${protocol}${href}`;
|
|
64
|
-
}
|
|
65
|
-
return href;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
52
|
/**
|
|
69
53
|
* Find the indicated constructor in scope, then return a function which invokes
|
|
70
54
|
* it with `new`.
|
|
@@ -88,6 +72,49 @@ export async function constructor(...keys) {
|
|
|
88
72
|
}
|
|
89
73
|
constructor.toString = () => "«ops.constructor»";
|
|
90
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Given a protocol, a host, and a list of keys, construct an href.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} protocol
|
|
79
|
+
* @param {string} host
|
|
80
|
+
* @param {...string|Symbol} keys
|
|
81
|
+
*/
|
|
82
|
+
function constructHref(protocol, host, ...keys) {
|
|
83
|
+
// Remove trailing slashes
|
|
84
|
+
const baseKeys = keys.map((key) => trailingSlash.remove(key));
|
|
85
|
+
let href = [host, ...baseKeys].join("/");
|
|
86
|
+
if (!href.startsWith(protocol)) {
|
|
87
|
+
if (!href.startsWith("//")) {
|
|
88
|
+
href = `//${href}`;
|
|
89
|
+
}
|
|
90
|
+
href = `${protocol}${href}`;
|
|
91
|
+
}
|
|
92
|
+
return href;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Given a protocol, a host, and a list of keys, construct an href.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} protocol
|
|
99
|
+
* @param {import("../../index.ts").Constructor<AsyncTree>} treeClass
|
|
100
|
+
* @param {AsyncTree|null} parent
|
|
101
|
+
* @param {string} host
|
|
102
|
+
* @param {...string|Symbol} keys
|
|
103
|
+
*/
|
|
104
|
+
async function constructSiteTree(protocol, treeClass, parent, host, ...keys) {
|
|
105
|
+
// If the last key doesn't end in a slash, remove it for now.
|
|
106
|
+
let lastKey;
|
|
107
|
+
if (keys.length > 0 && keys.at(-1) && !trailingSlash.has(keys.at(-1))) {
|
|
108
|
+
lastKey = keys.pop();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const href = constructHref(protocol, host, ...keys);
|
|
112
|
+
let result = new (HandleExtensionsTransform(treeClass))(href);
|
|
113
|
+
result.parent = parent;
|
|
114
|
+
|
|
115
|
+
return lastKey ? result.get(lastKey) : result;
|
|
116
|
+
}
|
|
117
|
+
|
|
91
118
|
/**
|
|
92
119
|
* Fetch the resource at the given href.
|
|
93
120
|
*
|
|
@@ -105,7 +132,7 @@ async function fetchResponse(href) {
|
|
|
105
132
|
const url = new URL(href);
|
|
106
133
|
const filename = url.pathname.split("/").pop();
|
|
107
134
|
if (this && filename) {
|
|
108
|
-
buffer = await
|
|
135
|
+
buffer = await handleExtension(this, buffer, filename);
|
|
109
136
|
}
|
|
110
137
|
|
|
111
138
|
return buffer;
|
|
@@ -257,6 +284,18 @@ export async function object(...entries) {
|
|
|
257
284
|
}
|
|
258
285
|
object.toString = () => "«ops.object»";
|
|
259
286
|
|
|
287
|
+
/**
|
|
288
|
+
* An open tree with JSON Keys via HTTPS.
|
|
289
|
+
*
|
|
290
|
+
* @this {AsyncTree|null}
|
|
291
|
+
* @param {string} host
|
|
292
|
+
* @param {...string|Symbol} keys
|
|
293
|
+
*/
|
|
294
|
+
export function openSite(host, ...keys) {
|
|
295
|
+
return constructSiteTree("https:", OpenSiteTree, this, host, ...keys);
|
|
296
|
+
}
|
|
297
|
+
openSite.toString = () => "«ops.openSite»";
|
|
298
|
+
|
|
260
299
|
/**
|
|
261
300
|
* Look up the given key in the scope for the current tree.
|
|
262
301
|
*
|
|
@@ -303,10 +342,7 @@ export const traverse = Tree.traverseOrThrow;
|
|
|
303
342
|
* @param {...string|Symbol} keys
|
|
304
343
|
*/
|
|
305
344
|
export function treeHttp(host, ...keys) {
|
|
306
|
-
|
|
307
|
-
let result = new (HandleExtensionsTransform(SiteTree))(href);
|
|
308
|
-
result.parent = this;
|
|
309
|
-
return result;
|
|
345
|
+
return constructSiteTree("http:", SiteTree, this, host, ...keys);
|
|
310
346
|
}
|
|
311
347
|
treeHttp.toString = () => "«ops.treeHttp»";
|
|
312
348
|
|
|
@@ -318,9 +354,16 @@ treeHttp.toString = () => "«ops.treeHttp»";
|
|
|
318
354
|
* @param {...string|Symbol} keys
|
|
319
355
|
*/
|
|
320
356
|
export function treeHttps(host, ...keys) {
|
|
321
|
-
|
|
322
|
-
let result = new (HandleExtensionsTransform(SiteTree))(href);
|
|
323
|
-
result.parent = this;
|
|
324
|
-
return result;
|
|
357
|
+
return constructSiteTree("https:", SiteTree, this, host, ...keys);
|
|
325
358
|
}
|
|
326
359
|
treeHttps.toString = () => "«ops.treeHttps»";
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* If the value is packed but has an unpack method, call it and return that as
|
|
363
|
+
* the result; otherwise, return the value as is.
|
|
364
|
+
*
|
|
365
|
+
* @param {any} value
|
|
366
|
+
*/
|
|
367
|
+
export async function unpack(value) {
|
|
368
|
+
return isUnpackable(value) ? value.unpack() : value;
|
|
369
|
+
}
|
|
@@ -36,11 +36,17 @@ describe("compile", () => {
|
|
|
36
36
|
await assertCompile("-1", -1);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
test("object", async () => {
|
|
39
|
+
test("sync object", async () => {
|
|
40
40
|
await assertCompile("{a:1, b:2}", { a: 1, b: 2 });
|
|
41
41
|
await assertCompile("{ a: { b: { c: 0 } } }", { a: { b: { c: 0 } } });
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
test("async object", async () => {
|
|
45
|
+
const fn = compile.expression("{ a: { b = name }}");
|
|
46
|
+
const object = await fn.call(shared);
|
|
47
|
+
assert.deepEqual(await object["a/"].b, "Alice");
|
|
48
|
+
});
|
|
49
|
+
|
|
44
50
|
test("templateDocument", async () => {
|
|
45
51
|
const fn = compile.templateDocument("Documents can contain ` backticks");
|
|
46
52
|
const templateFn = await fn.call(shared);
|