@weborigami/language 0.0.73 → 0.2.0
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/index.ts +1 -0
- package/main.js +2 -2
- package/package.json +6 -4
- package/src/compiler/compile.js +42 -17
- package/src/compiler/origami.pegjs +248 -182
- package/src/compiler/parse.js +1569 -1231
- package/src/compiler/parserHelpers.js +180 -48
- package/src/runtime/HandleExtensionsTransform.js +1 -1
- package/src/runtime/ImportModulesMixin.js +1 -1
- package/src/runtime/codeFragment.js +2 -2
- package/src/runtime/errors.js +104 -0
- package/src/runtime/evaluate.js +3 -3
- package/src/runtime/expressionObject.js +8 -5
- package/src/runtime/{extensions.js → handlers.js} +6 -24
- package/src/runtime/internal.js +1 -0
- package/src/runtime/ops.js +156 -185
- package/src/runtime/typos.js +71 -0
- package/test/cases/ReadMe.md +1 -0
- package/test/cases/conditionalExpression.yaml +101 -0
- package/test/cases/logicalAndExpression.yaml +146 -0
- package/test/cases/logicalOrExpression.yaml +145 -0
- package/test/cases/nullishCoalescingExpression.yaml +105 -0
- package/test/compiler/compile.test.js +7 -7
- package/test/compiler/parse.test.js +506 -294
- package/test/generated/conditionalExpression.test.js +58 -0
- package/test/generated/logicalAndExpression.test.js +80 -0
- package/test/generated/logicalOrExpression.test.js +78 -0
- package/test/generated/nullishCoalescingExpression.test.js +64 -0
- package/test/generator/generateTests.js +80 -0
- package/test/generator/oriEval.js +15 -0
- package/test/runtime/fixtures/templates/greet.orit +1 -1
- package/test/runtime/{extensions.test.js → handlers.test.js} +2 -2
- package/test/runtime/ops.test.js +129 -26
- package/test/runtime/typos.test.js +21 -0
- package/src/runtime/formatError.js +0 -56
|
@@ -1,45 +1,81 @@
|
|
|
1
1
|
import { trailingSlash } from "@weborigami/async-tree";
|
|
2
|
+
import codeFragment from "../runtime/codeFragment.js";
|
|
2
3
|
import * as ops from "../runtime/ops.js";
|
|
3
4
|
|
|
4
5
|
// Parser helpers
|
|
5
6
|
|
|
7
|
+
/** @typedef {import("../../index.ts").Code} Code */
|
|
8
|
+
|
|
9
|
+
// Marker for a reference that may be a builtin or a scope reference
|
|
10
|
+
export const undetermined = Symbol("undetermined");
|
|
11
|
+
|
|
12
|
+
const builtinRegex = /^[A-Za-z][A-Za-z0-9]*$/;
|
|
13
|
+
|
|
6
14
|
/**
|
|
7
15
|
* If a parse result is an object that will be evaluated at runtime, attach the
|
|
8
16
|
* location of the source code that produced it for debugging and error messages.
|
|
17
|
+
*
|
|
18
|
+
* @param {Code} code
|
|
19
|
+
* @param {any} location
|
|
9
20
|
*/
|
|
10
|
-
export function annotate(
|
|
11
|
-
if (typeof
|
|
12
|
-
|
|
21
|
+
export function annotate(code, location) {
|
|
22
|
+
if (typeof code === "object" && code !== null && location) {
|
|
23
|
+
code.location = location;
|
|
24
|
+
code.source = codeFragment(location);
|
|
13
25
|
}
|
|
14
|
-
return
|
|
26
|
+
return code;
|
|
15
27
|
}
|
|
16
28
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The indicated code is being used to define a property named by the given key.
|
|
31
|
+
* Rewrite any [ops.scope, key] calls to be [ops.inherited, key] to avoid
|
|
32
|
+
* infinite recursion.
|
|
33
|
+
*
|
|
34
|
+
* @param {Code} code
|
|
35
|
+
* @param {string} key
|
|
36
|
+
*/
|
|
20
37
|
function avoidRecursivePropertyCalls(code, key) {
|
|
21
38
|
if (!(code instanceof Array)) {
|
|
22
39
|
return code;
|
|
23
40
|
}
|
|
41
|
+
/** @type {Code} */
|
|
24
42
|
let modified;
|
|
25
43
|
if (
|
|
26
44
|
code[0] === ops.scope &&
|
|
27
45
|
trailingSlash.remove(code[1]) === trailingSlash.remove(key)
|
|
28
46
|
) {
|
|
29
47
|
// Rewrite to avoid recursion
|
|
48
|
+
// @ts-ignore
|
|
30
49
|
modified = [ops.inherited, code[1]];
|
|
31
50
|
} else if (code[0] === ops.lambda && code[1].includes(key)) {
|
|
32
51
|
// Lambda that defines the key; don't rewrite
|
|
33
52
|
return code;
|
|
34
53
|
} else {
|
|
35
54
|
// Process any nested code
|
|
55
|
+
// @ts-ignore
|
|
36
56
|
modified = code.map((value) => avoidRecursivePropertyCalls(value, key));
|
|
37
57
|
}
|
|
38
|
-
|
|
39
|
-
modified.location = code.location;
|
|
58
|
+
annotate(modified, code.location);
|
|
40
59
|
return modified;
|
|
41
60
|
}
|
|
42
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Downgrade a potential builtin reference to a scope reference.
|
|
64
|
+
*
|
|
65
|
+
* @param {Code} code
|
|
66
|
+
*/
|
|
67
|
+
export function downgradeReference(code) {
|
|
68
|
+
if (code && code.length === 2 && code[0] === undetermined) {
|
|
69
|
+
/** @type {Code} */
|
|
70
|
+
// @ts-ignore
|
|
71
|
+
const result = [ops.scope, code[1]];
|
|
72
|
+
annotate(result, code.location);
|
|
73
|
+
return result;
|
|
74
|
+
} else {
|
|
75
|
+
return code;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
43
79
|
// Return true if the code will generate an async object.
|
|
44
80
|
function isCodeForAsyncObject(code) {
|
|
45
81
|
if (!(code instanceof Array)) {
|
|
@@ -89,56 +125,111 @@ export function makeArray(entries) {
|
|
|
89
125
|
}
|
|
90
126
|
|
|
91
127
|
/**
|
|
92
|
-
*
|
|
128
|
+
* Create a chain of binary operators. The head is the first value, and the tail
|
|
129
|
+
* is an array of [operator, value] pairs.
|
|
93
130
|
*
|
|
131
|
+
* @param {Code} head
|
|
132
|
+
* @param {[any, Code][]} tail
|
|
133
|
+
*/
|
|
134
|
+
export function makeBinaryOperatorChain(head, tail) {
|
|
135
|
+
/** @type {Code} */
|
|
136
|
+
let value = head;
|
|
137
|
+
for (const [operatorToken, right] of tail) {
|
|
138
|
+
const left = value;
|
|
139
|
+
const operators = {
|
|
140
|
+
"===": ops.strictEqual,
|
|
141
|
+
"!==": ops.notStrictEqual,
|
|
142
|
+
"==": ops.equal,
|
|
143
|
+
"!=": ops.notEqual,
|
|
144
|
+
};
|
|
145
|
+
const op = operators[operatorToken];
|
|
146
|
+
// @ts-ignore
|
|
147
|
+
value = [op, left, right];
|
|
148
|
+
value.location = {
|
|
149
|
+
source: left.location.source,
|
|
150
|
+
start: left.location.start,
|
|
151
|
+
end: right.location.end,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
94
158
|
* @param {Code} target
|
|
95
|
-
* @param {
|
|
96
|
-
* @returns
|
|
159
|
+
* @param {any[]} args
|
|
97
160
|
*/
|
|
98
|
-
export function
|
|
161
|
+
export function makeCall(target, args) {
|
|
99
162
|
if (!(target instanceof Array)) {
|
|
100
163
|
const error = new SyntaxError(`Can't call this like a function: ${target}`);
|
|
101
|
-
/** @type {any} */ (error).location = location;
|
|
164
|
+
/** @type {any} */ (error).location = /** @type {any} */ (target).location;
|
|
102
165
|
throw error;
|
|
103
166
|
}
|
|
104
167
|
|
|
105
|
-
let value = target;
|
|
106
168
|
const source = target.location.source;
|
|
107
|
-
// The chain is an array of arguments (which are themselves arrays). We
|
|
108
|
-
// successively apply the top-level elements of that chain to build up the
|
|
109
|
-
// function composition.
|
|
110
169
|
let start = target.location.start;
|
|
111
170
|
let end = target.location.end;
|
|
112
|
-
for (const args of chain) {
|
|
113
|
-
/** @type {Code} */
|
|
114
|
-
let fnCall;
|
|
115
171
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// Create a location spanning the newly-constructed function call.
|
|
128
|
-
if (args instanceof Array) {
|
|
129
|
-
if (args.location) {
|
|
130
|
-
end = args.location.end;
|
|
131
|
-
} else {
|
|
132
|
-
throw "Internal parser error: no location for function call argument";
|
|
172
|
+
let fnCall;
|
|
173
|
+
if (args[0] === ops.traverse) {
|
|
174
|
+
let tree = target;
|
|
175
|
+
|
|
176
|
+
if (tree[0] === undetermined) {
|
|
177
|
+
// In a traversal, downgrade ops.builtin references to ops.scope
|
|
178
|
+
tree = downgradeReference(tree);
|
|
179
|
+
if (tree[0] === ops.scope && !trailingSlash.has(tree[1])) {
|
|
180
|
+
// Target didn't parse with a trailing slash; add one
|
|
181
|
+
tree[1] = trailingSlash.add(tree[1]);
|
|
133
182
|
}
|
|
134
183
|
}
|
|
135
184
|
|
|
136
|
-
|
|
185
|
+
if (args.length > 1) {
|
|
186
|
+
// Regular traverse
|
|
187
|
+
const keys = args.slice(1);
|
|
188
|
+
fnCall = [ops.traverse, tree, ...keys];
|
|
189
|
+
} else {
|
|
190
|
+
// Traverse without arguments equates to unpack
|
|
191
|
+
fnCall = [ops.unpack, tree];
|
|
192
|
+
}
|
|
193
|
+
} else if (args[0] === ops.template) {
|
|
194
|
+
// Tagged template
|
|
195
|
+
fnCall = [upgradeReference(target), ...args.slice(1)];
|
|
196
|
+
} else {
|
|
197
|
+
// Function call with explicit or implicit parentheses
|
|
198
|
+
fnCall = [upgradeReference(target), ...args];
|
|
199
|
+
}
|
|
137
200
|
|
|
138
|
-
|
|
201
|
+
// Create a location spanning the newly-constructed function call.
|
|
202
|
+
if (args instanceof Array) {
|
|
203
|
+
// @ts-ignore
|
|
204
|
+
end = args.location?.end ?? args.at(-1)?.location?.end;
|
|
205
|
+
if (end === undefined) {
|
|
206
|
+
throw "Internal parser error: no location for function call argument";
|
|
207
|
+
}
|
|
139
208
|
}
|
|
140
209
|
|
|
141
|
-
|
|
210
|
+
// @ts-ignore
|
|
211
|
+
annotate(fnCall, { start, source, end });
|
|
212
|
+
|
|
213
|
+
return fnCall;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* For functions that short-circuit arguments, we need to defer evaluation of
|
|
218
|
+
* the arguments until the function is called. Exception: if the argument is a
|
|
219
|
+
* literal, we leave it alone.
|
|
220
|
+
*
|
|
221
|
+
* @param {any[]} args
|
|
222
|
+
*/
|
|
223
|
+
export function makeDeferredArguments(args) {
|
|
224
|
+
return args.map((arg) => {
|
|
225
|
+
if (arg instanceof Array && arg[0] === ops.literal) {
|
|
226
|
+
return arg;
|
|
227
|
+
}
|
|
228
|
+
const fn = [ops.lambda, [], arg];
|
|
229
|
+
// @ts-ignore
|
|
230
|
+
annotate(fn, arg.location);
|
|
231
|
+
return fn;
|
|
232
|
+
});
|
|
142
233
|
}
|
|
143
234
|
|
|
144
235
|
export function makeObject(entries, op) {
|
|
@@ -189,13 +280,15 @@ export function makeObject(entries, op) {
|
|
|
189
280
|
}
|
|
190
281
|
|
|
191
282
|
// Similar to a function call, but the order is reversed.
|
|
192
|
-
export function makePipeline(
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
283
|
+
export function makePipeline(arg, fn) {
|
|
284
|
+
const upgraded = upgradeReference(fn);
|
|
285
|
+
const result = makeCall(upgraded, [arg]);
|
|
286
|
+
const source = fn.location.source;
|
|
287
|
+
let start = arg.location.start;
|
|
288
|
+
let end = fn.location.end;
|
|
289
|
+
// @ts-ignore
|
|
290
|
+
annotate(result, { start, source, end });
|
|
291
|
+
return result;
|
|
199
292
|
}
|
|
200
293
|
|
|
201
294
|
// Define a property on an object.
|
|
@@ -204,6 +297,21 @@ export function makeProperty(key, value) {
|
|
|
204
297
|
return [key, modified];
|
|
205
298
|
}
|
|
206
299
|
|
|
300
|
+
export function makeReference(identifier) {
|
|
301
|
+
// We can't know for sure that an identifier is a builtin reference until we
|
|
302
|
+
// see whether it's being called as a function.
|
|
303
|
+
let op;
|
|
304
|
+
if (builtinRegex.test(identifier)) {
|
|
305
|
+
op = identifier.endsWith(":")
|
|
306
|
+
? // Namespace is always a builtin reference
|
|
307
|
+
ops.builtin
|
|
308
|
+
: undetermined;
|
|
309
|
+
} else {
|
|
310
|
+
op = ops.scope;
|
|
311
|
+
}
|
|
312
|
+
return [op, identifier];
|
|
313
|
+
}
|
|
314
|
+
|
|
207
315
|
export function makeTemplate(op, head, tail) {
|
|
208
316
|
const strings = [head];
|
|
209
317
|
const values = [];
|
|
@@ -213,3 +321,27 @@ export function makeTemplate(op, head, tail) {
|
|
|
213
321
|
}
|
|
214
322
|
return [op, [ops.literal, strings], ...values];
|
|
215
323
|
}
|
|
324
|
+
|
|
325
|
+
export function makeUnaryOperatorCall(operator, value) {
|
|
326
|
+
const operators = {
|
|
327
|
+
"!": ops.logicalNot,
|
|
328
|
+
};
|
|
329
|
+
return [operators[operator], value];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Upgrade a potential builtin reference to an actual builtin reference.
|
|
334
|
+
*
|
|
335
|
+
* @param {Code} code
|
|
336
|
+
*/
|
|
337
|
+
export function upgradeReference(code) {
|
|
338
|
+
if (code.length === 2 && code[0] === undetermined) {
|
|
339
|
+
/** @type {Code} */
|
|
340
|
+
// @ts-ignore
|
|
341
|
+
const result = [ops.builtin, code[1]];
|
|
342
|
+
annotate(result, code.location);
|
|
343
|
+
return result;
|
|
344
|
+
} else {
|
|
345
|
+
return code;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
@@ -1,7 +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 "./
|
|
4
|
+
import { maybeOrigamiSourceCode } from "./errors.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
@@ -7,8 +7,8 @@ export default function codeFragment(location) {
|
|
|
7
7
|
: // Use entire source
|
|
8
8
|
source.text;
|
|
9
9
|
|
|
10
|
-
//
|
|
11
|
-
fragment = fragment.replace(/(\n|\s\s+)+/g, "");
|
|
10
|
+
// Replace newlines and whitespace runs with a single space.
|
|
11
|
+
fragment = fragment.replace(/(\n|\s\s+)+/g, " ");
|
|
12
12
|
|
|
13
13
|
// If longer than 80 characters, truncate with an ellipsis.
|
|
14
14
|
if (fragment.length > 80) {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Text we look for in an error stack to guess whether a given line represents a
|
|
2
|
+
|
|
3
|
+
import { scope as scopeFn, trailingSlash } from "@weborigami/async-tree";
|
|
4
|
+
import codeFragment from "./codeFragment.js";
|
|
5
|
+
import { typos } from "./typos.js";
|
|
6
|
+
|
|
7
|
+
// function in the Origami source code.
|
|
8
|
+
const origamiSourceSignals = [
|
|
9
|
+
"async-tree/src/",
|
|
10
|
+
"language/src/",
|
|
11
|
+
"origami/src/",
|
|
12
|
+
"at Scope.evaluate",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export async function builtinReferenceError(tree, builtins, key) {
|
|
16
|
+
const messages = [
|
|
17
|
+
`"${key}" is being called as if it were a builtin function, but it's not.`,
|
|
18
|
+
];
|
|
19
|
+
// See if the key is in scope (but not as a builtin)
|
|
20
|
+
const scope = scopeFn(tree);
|
|
21
|
+
const value = await scope.get(key);
|
|
22
|
+
if (value === undefined) {
|
|
23
|
+
const typos = await formatScopeTypos(builtins, key);
|
|
24
|
+
messages.push(typos);
|
|
25
|
+
} else {
|
|
26
|
+
messages.push(`Use "${key}/" instead.`);
|
|
27
|
+
}
|
|
28
|
+
const message = messages.join(" ");
|
|
29
|
+
return new ReferenceError(message);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Format an error for display in the console.
|
|
34
|
+
*
|
|
35
|
+
* @param {Error} error
|
|
36
|
+
*/
|
|
37
|
+
export function formatError(error) {
|
|
38
|
+
let message;
|
|
39
|
+
if (error.stack) {
|
|
40
|
+
// Display the stack only until we reach the Origami source code.
|
|
41
|
+
message = "";
|
|
42
|
+
let lines = error.stack.split("\n");
|
|
43
|
+
for (let i = 0; i < lines.length; i++) {
|
|
44
|
+
const line = lines[i];
|
|
45
|
+
if (maybeOrigamiSourceCode(line)) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
if (message) {
|
|
49
|
+
message += "\n";
|
|
50
|
+
}
|
|
51
|
+
message += lines[i];
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
message = error.toString();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Add location
|
|
58
|
+
let location = /** @type {any} */ (error).location;
|
|
59
|
+
if (location) {
|
|
60
|
+
const fragment = codeFragment(location);
|
|
61
|
+
let { source, start } = location;
|
|
62
|
+
|
|
63
|
+
message += `\nevaluating: ${fragment}`;
|
|
64
|
+
if (typeof source === "object" && source.url) {
|
|
65
|
+
message += `\n at ${source.url.href}:${start.line}:${start.column}`;
|
|
66
|
+
} else if (source.text.includes("\n")) {
|
|
67
|
+
message += `\n at line ${start.line}, column ${start.column}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return message;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function formatScopeTypos(scope, key) {
|
|
74
|
+
const keys = await scopeTypos(scope, key);
|
|
75
|
+
// Don't match deprecated keys
|
|
76
|
+
const filtered = keys.filter((key) => !key.startsWith("@"));
|
|
77
|
+
if (filtered.length === 0) {
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
const quoted = filtered.map((key) => `"${key}"`);
|
|
81
|
+
const list = quoted.join(", ");
|
|
82
|
+
return `Maybe you meant ${list}?`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function maybeOrigamiSourceCode(text) {
|
|
86
|
+
return origamiSourceSignals.some((signal) => text.includes(signal));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function scopeReferenceError(scope, key) {
|
|
90
|
+
const messages = [
|
|
91
|
+
`"${key}" is not in scope.`,
|
|
92
|
+
await formatScopeTypos(scope, key),
|
|
93
|
+
];
|
|
94
|
+
const message = messages.join(" ");
|
|
95
|
+
return new ReferenceError(message);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Return all possible typos for `key` in scope
|
|
99
|
+
async function scopeTypos(scope, key) {
|
|
100
|
+
const scopeKeys = [...(await scope.keys())];
|
|
101
|
+
const normalizedScopeKeys = scopeKeys.map((key) => trailingSlash.remove(key));
|
|
102
|
+
const normalizedKey = trailingSlash.remove(key);
|
|
103
|
+
return typos(normalizedKey, normalizedScopeKeys);
|
|
104
|
+
}
|
package/src/runtime/evaluate.js
CHANGED
|
@@ -68,10 +68,10 @@ export default async function evaluate(code) {
|
|
|
68
68
|
if (!error.location) {
|
|
69
69
|
// Attach the location of the code we tried to evaluate.
|
|
70
70
|
error.location =
|
|
71
|
-
error.position !== undefined
|
|
71
|
+
error.position !== undefined && code[error.position + 1]?.location
|
|
72
72
|
? // Use location of the argument with the given position (need to
|
|
73
73
|
// offset by 1 to skip the function).
|
|
74
|
-
code[error.position + 1]
|
|
74
|
+
code[error.position + 1]?.location
|
|
75
75
|
: // Use overall location.
|
|
76
76
|
code.location;
|
|
77
77
|
}
|
|
@@ -85,7 +85,7 @@ export default async function evaluate(code) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// To aid debugging, add the code to the result.
|
|
88
|
-
if (Object.isExtensible(result)
|
|
88
|
+
if (Object.isExtensible(result)) {
|
|
89
89
|
try {
|
|
90
90
|
if (code.location && !result[sourceSymbol]) {
|
|
91
91
|
Object.defineProperty(result, sourceSymbol, {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ObjectTree, symbols } from "@weborigami/async-tree";
|
|
2
|
-
import {
|
|
1
|
+
import { extension, ObjectTree, symbols, Tree } from "@weborigami/async-tree";
|
|
2
|
+
import { handleExtension } from "./handlers.js";
|
|
3
3
|
import { evaluate, ops } from "./internal.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -22,6 +22,9 @@ import { evaluate, ops } from "./internal.js";
|
|
|
22
22
|
export default async function expressionObject(entries, parent) {
|
|
23
23
|
// Create the object and set its parent
|
|
24
24
|
const object = {};
|
|
25
|
+
if (parent !== null && !Tree.isAsyncTree(parent)) {
|
|
26
|
+
throw new TypeError(`Parent must be an AsyncTree or null`);
|
|
27
|
+
}
|
|
25
28
|
Object.defineProperty(object, symbols.parent, {
|
|
26
29
|
configurable: true,
|
|
27
30
|
enumerable: false,
|
|
@@ -37,8 +40,8 @@ export default async function expressionObject(entries, parent) {
|
|
|
37
40
|
// array), we need to define a getter -- but if that code takes the form
|
|
38
41
|
// [ops.getter, <primitive>], we can define a regular property.
|
|
39
42
|
let defineProperty;
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
43
|
+
const extname = extension.extname(key);
|
|
44
|
+
if (extname) {
|
|
42
45
|
defineProperty = false;
|
|
43
46
|
} else if (!(value instanceof Array)) {
|
|
44
47
|
defineProperty = true;
|
|
@@ -76,7 +79,7 @@ export default async function expressionObject(entries, parent) {
|
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
let get;
|
|
79
|
-
if (
|
|
82
|
+
if (extname) {
|
|
80
83
|
// Key has extension, getter will invoke code then attach unpack method
|
|
81
84
|
get = async () => {
|
|
82
85
|
tree ??= new ObjectTree(object);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
box,
|
|
3
|
+
extension,
|
|
3
4
|
isPacked,
|
|
4
5
|
isStringLike,
|
|
5
6
|
isUnpackable,
|
|
@@ -13,27 +14,6 @@ import {
|
|
|
13
14
|
// Track extensions handlers for a given containing tree.
|
|
14
15
|
const handlersForContainer = new Map();
|
|
15
16
|
|
|
16
|
-
/**
|
|
17
|
-
* If the given path ends in an extension, return it. Otherwise, return the
|
|
18
|
-
* empty string.
|
|
19
|
-
*
|
|
20
|
-
* This is meant as a basic replacement for the standard Node `path.extname`.
|
|
21
|
-
* That standard function inaccurately returns an extension for a path that
|
|
22
|
-
* includes a near-final extension but ends in a final slash, like `foo.txt/`.
|
|
23
|
-
* Node thinks that path has a ".txt" extension, but for our purposes it
|
|
24
|
-
* doesn't.
|
|
25
|
-
*
|
|
26
|
-
* @param {string} path
|
|
27
|
-
*/
|
|
28
|
-
export function extname(path) {
|
|
29
|
-
// We want at least one character before the dot, then a dot, then a non-empty
|
|
30
|
-
// sequence of characters after the dot that aren't slahes or dots.
|
|
31
|
-
const extnameRegex = /[^/](?<ext>\.[^/\.]+)$/;
|
|
32
|
-
const match = String(path).match(extnameRegex);
|
|
33
|
-
const extension = match?.groups?.ext.toLowerCase() ?? "";
|
|
34
|
-
return extension;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
17
|
/**
|
|
38
18
|
* Find an extension handler for a file in the given container.
|
|
39
19
|
*
|
|
@@ -95,9 +75,11 @@ export async function handleExtension(parent, value, key) {
|
|
|
95
75
|
}
|
|
96
76
|
|
|
97
77
|
// Special case: `.ori.<ext>` extensions are Origami documents.
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
78
|
+
const extname = key.match(/\.ori\.\S+$/)
|
|
79
|
+
? ".oridocument"
|
|
80
|
+
: extension.extname(key);
|
|
81
|
+
if (extname) {
|
|
82
|
+
const handler = await getExtensionHandler(parent, extname);
|
|
101
83
|
if (handler) {
|
|
102
84
|
if (hasSlash && handler.unpack) {
|
|
103
85
|
// Key like `data.json/` ends in slash -- unpack immediately
|
package/src/runtime/internal.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
//
|
|
7
7
|
// About this pattern: https://medium.com/visual-development/how-to-fix-nasty-circular-dependency-issues-once-and-for-all-in-javascript-typescript-a04c987cf0de
|
|
8
8
|
//
|
|
9
|
+
// Note: to avoid having VS Code auto-sort the imports, keep lines between them.
|
|
9
10
|
|
|
10
11
|
export * as ops from "./ops.js";
|
|
11
12
|
|