@weborigami/language 0.6.8 → 0.6.10
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 +19 -4
- package/main.js +5 -2
- package/package.json +2 -2
- package/src/compiler/compile.js +11 -3
- package/src/compiler/origami.pegjs +4 -2
- package/src/compiler/parse.js +77 -68
- package/src/compiler/parserHelpers.js +13 -13
- package/src/handlers/getPackedPath.js +17 -0
- package/src/handlers/jpeg_handler.js +5 -0
- package/src/handlers/js_handler.js +3 -3
- package/src/handlers/json_handler.js +3 -1
- package/src/handlers/tsv_handler.js +1 -1
- package/src/handlers/yaml_handler.js +1 -1
- package/src/project/jsGlobals.js +3 -3
- package/src/protocols/package.js +3 -3
- package/src/runtime/asyncStorage.js +7 -0
- package/src/runtime/codeFragment.js +4 -3
- package/src/runtime/errors.js +82 -129
- package/src/runtime/evaluate.js +8 -77
- package/src/runtime/execute.js +82 -0
- package/src/runtime/explainReferenceError.js +248 -0
- package/src/runtime/explainTraverseError.js +77 -0
- package/src/runtime/expressionFunction.js +8 -7
- package/src/runtime/expressionObject.js +9 -6
- package/src/runtime/handleExtension.js +22 -8
- package/src/runtime/internal.js +1 -1
- package/src/runtime/interop.js +15 -0
- package/src/runtime/ops.js +24 -19
- package/src/runtime/symbols.js +0 -1
- package/src/runtime/typos.js +22 -3
- package/test/compiler/compile.test.js +7 -103
- package/test/compiler/parse.test.js +38 -31
- package/test/project/fixtures/withPackageJson/subfolder/README.md +1 -0
- package/test/runtime/errors.test.js +296 -0
- package/test/runtime/evaluate.test.js +110 -34
- package/test/runtime/execute.test.js +41 -0
- package/test/runtime/expressionObject.test.js +4 -4
- package/test/runtime/ops.test.js +36 -35
- package/test/runtime/typos.test.js +2 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import {
|
|
2
|
+
pathFromKeys,
|
|
3
|
+
scope,
|
|
4
|
+
trailingSlash,
|
|
5
|
+
Tree,
|
|
6
|
+
} from "@weborigami/async-tree";
|
|
7
|
+
import { ops } from "./internal.js";
|
|
8
|
+
import { typos } from "./typos.js";
|
|
9
|
+
|
|
10
|
+
// Doesn't include `/` because that would have been handled as a path separator
|
|
11
|
+
const binaryOperatorRegex =
|
|
12
|
+
/!==|!=|%|&&|&|\*\*|\*|\+|-|\/|<<|<|<=|===|==|>>>|>>|>=|>|\^|\|\||\|/g;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Try to provide a more helpful message for a ReferenceError by analyzing the
|
|
16
|
+
* code and suggesting possible typos.
|
|
17
|
+
*
|
|
18
|
+
* @param {import("../../index.ts").AnnotatedCode} code
|
|
19
|
+
* @param {import("../../index.ts").RuntimeState} state
|
|
20
|
+
*/
|
|
21
|
+
export default async function explainReferenceError(code, state) {
|
|
22
|
+
if (!state) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const stateKeys = await getStateKeys(state);
|
|
27
|
+
|
|
28
|
+
if (code[0] === ops.property) {
|
|
29
|
+
// An inner property access returned undefined.
|
|
30
|
+
// Might be a global+extension or local+extension.
|
|
31
|
+
const explanation = await accidentalReferenceExplainer(
|
|
32
|
+
code.source,
|
|
33
|
+
stateKeys,
|
|
34
|
+
);
|
|
35
|
+
return explanation;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// See if the code looks like an external scope reference that failed
|
|
39
|
+
let key;
|
|
40
|
+
if (code[0] === ops.cache) {
|
|
41
|
+
// External scope reference
|
|
42
|
+
const scopeCall = code[3].slice(1); // drop the ops.scope
|
|
43
|
+
const keys = scopeCall.map((part) => part[1]);
|
|
44
|
+
const path = pathFromKeys(keys);
|
|
45
|
+
|
|
46
|
+
if (keys.length > 1) {
|
|
47
|
+
return `This path returned undefined: ${path}`;
|
|
48
|
+
}
|
|
49
|
+
key = keys[0];
|
|
50
|
+
} else if (code[0]?.[0] === ops.scope) {
|
|
51
|
+
// Simple scope reference
|
|
52
|
+
key = code[1][1];
|
|
53
|
+
} else {
|
|
54
|
+
// Generic reference error, can't offer help
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
key = trailingSlash.remove(key);
|
|
59
|
+
if (typeof key !== "string") {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Common case of a single key
|
|
64
|
+
let message = `"${key}" is not in scope.`;
|
|
65
|
+
|
|
66
|
+
const explainers = [
|
|
67
|
+
mathExplainer,
|
|
68
|
+
qualifiedReferenceExplainer,
|
|
69
|
+
typoExplainer,
|
|
70
|
+
];
|
|
71
|
+
let explanation;
|
|
72
|
+
for (const explainer of explainers) {
|
|
73
|
+
explanation = await explainer(key, stateKeys);
|
|
74
|
+
if (explanation) {
|
|
75
|
+
message += "\n" + explanation;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return message;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Handle a reference that worked but maybe shouldn't have:
|
|
85
|
+
*
|
|
86
|
+
* - a global + extension like `performance.html` (`performance` is a global)
|
|
87
|
+
* - a local + extension like `posts.md` (where `posts` is a local variable)
|
|
88
|
+
*
|
|
89
|
+
* In either case, suggest using angle brackets.
|
|
90
|
+
*/
|
|
91
|
+
async function accidentalReferenceExplainer(key, stateKeys) {
|
|
92
|
+
const parts = key.split(".");
|
|
93
|
+
if (parts.length !== 2) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const extensionHandlers = stateKeys.global.filter((globalKey) =>
|
|
98
|
+
globalKey.endsWith("_handler"),
|
|
99
|
+
);
|
|
100
|
+
const extensions = extensionHandlers.map((handler) => handler.slice(0, -8));
|
|
101
|
+
if (!extensions.includes(parts[1])) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (stateKeys.global.includes(parts[0])) {
|
|
106
|
+
return `"${parts[0]}" is a global, but "${parts[1]}" looks like a file extension.
|
|
107
|
+
If you intended to reference a file, use angle brackets: <${key}>`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (stateKeys.object.includes(parts[0])) {
|
|
111
|
+
return `"${key}" looks like a file reference, but is matching the local object property "${parts[0]}".
|
|
112
|
+
If you intended to reference a file, use angle brackets: <${key}>`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (stateKeys.stack.includes(parts[0])) {
|
|
116
|
+
return `"${key}" looks like a file reference, but is matching the local parameter "${parts[0]}".
|
|
117
|
+
If you intended to reference a file, use angle brackets: <${key}>`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Return global, local, and object keys in scope for the given state
|
|
124
|
+
async function getStateKeys(state) {
|
|
125
|
+
const { globals, parent, object, stack } = state;
|
|
126
|
+
const objectScope = object ? await scope(object) : null;
|
|
127
|
+
const parentScope = parent ? await scope(parent) : null;
|
|
128
|
+
|
|
129
|
+
const globalKeys = globals ? Object.keys(globals) : [];
|
|
130
|
+
const objectKeys = objectScope ? await Tree.keys(objectScope) : [];
|
|
131
|
+
const scopeKeys = parentScope ? await Tree.keys(parentScope) : [];
|
|
132
|
+
const stackKeys = stack?.map((frame) => Object.keys(frame)).flat() ?? [];
|
|
133
|
+
|
|
134
|
+
const qualifiedGlobal = [];
|
|
135
|
+
for (const globalKey of globalKeys) {
|
|
136
|
+
// Heuristic namespace test: name starts with capital, prototype is null (an
|
|
137
|
+
// exotic `Module` instance)
|
|
138
|
+
let global = globals[globalKey];
|
|
139
|
+
if (
|
|
140
|
+
/^[A-Z]/.test(globalKey) &&
|
|
141
|
+
global &&
|
|
142
|
+
Object.getPrototypeOf(global) === null
|
|
143
|
+
) {
|
|
144
|
+
for (const [key, value] of Object.entries(global)) {
|
|
145
|
+
if (typeof value === "function") {
|
|
146
|
+
qualifiedGlobal.push(`${globalKey}.${key}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const normalizedGlobalKeys = globalKeys.map((key) =>
|
|
153
|
+
trailingSlash.remove(key),
|
|
154
|
+
);
|
|
155
|
+
const normalizedObjectKeys = objectKeys.map((key) =>
|
|
156
|
+
trailingSlash.remove(key),
|
|
157
|
+
);
|
|
158
|
+
const normalizedScopeKeys = scopeKeys.map((key) => trailingSlash.remove(key));
|
|
159
|
+
const normalizedStackKeys = stackKeys.map((key) => trailingSlash.remove(key));
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
global: normalizedGlobalKeys,
|
|
163
|
+
object: normalizedObjectKeys,
|
|
164
|
+
qualifiedGlobal,
|
|
165
|
+
scope: normalizedScopeKeys,
|
|
166
|
+
stack: normalizedStackKeys,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* If it looks like a math operation, suggest adding spaces around the operator.
|
|
172
|
+
*/
|
|
173
|
+
function mathExplainer(key, stateKeys) {
|
|
174
|
+
if (!binaryOperatorRegex.test(key)) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Create a global version of the regex for replacing all operators
|
|
179
|
+
const withSpaces = key.replace(binaryOperatorRegex, " $& ");
|
|
180
|
+
return `If you intended a math operation, Origami requires spaces around the operator: "${withSpaces}"`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* If the key is an unqualified reference (`repeat`), but there's a qualified
|
|
185
|
+
* version in scope (`Origami.repeat`), suggest that.
|
|
186
|
+
*/
|
|
187
|
+
async function qualifiedReferenceExplainer(key, stateKeys) {
|
|
188
|
+
if (key.includes(".")) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const qualifiedKeys = stateKeys.qualifiedGlobal.filter((k) =>
|
|
193
|
+
k.endsWith("." + key),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
if (qualifiedKeys.length === 0) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let message = `Perhaps you intended`;
|
|
201
|
+
const list = qualifiedKeys.join(", ");
|
|
202
|
+
if (qualifiedKeys.length > 1) {
|
|
203
|
+
message += " one of these";
|
|
204
|
+
}
|
|
205
|
+
message += `: ${list}`;
|
|
206
|
+
|
|
207
|
+
return message;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Suggest possible typos for the given key based on the keys in scope.
|
|
212
|
+
*/
|
|
213
|
+
async function typoExplainer(key, stateKeys) {
|
|
214
|
+
const allKeys = [
|
|
215
|
+
...new Set([
|
|
216
|
+
...stateKeys.global,
|
|
217
|
+
...stateKeys.object,
|
|
218
|
+
...stateKeys.scope,
|
|
219
|
+
...stateKeys.stack,
|
|
220
|
+
]),
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
let firstPartTypos;
|
|
224
|
+
if (key.includes(".")) {
|
|
225
|
+
// Split off first part
|
|
226
|
+
const [firstPart] = key.split(".");
|
|
227
|
+
firstPartTypos = typos(firstPart, allKeys);
|
|
228
|
+
} else {
|
|
229
|
+
firstPartTypos = [];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const fullTypos = typos(key, allKeys);
|
|
233
|
+
const allTypos = [...new Set([...firstPartTypos, ...fullTypos])];
|
|
234
|
+
allTypos.sort();
|
|
235
|
+
|
|
236
|
+
if (allTypos.length === 0) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let message = `Perhaps you intended`;
|
|
241
|
+
const list = allTypos.join(", ");
|
|
242
|
+
if (allTypos.length > 1) {
|
|
243
|
+
message += " one of these";
|
|
244
|
+
}
|
|
245
|
+
message += `: ${list}`;
|
|
246
|
+
|
|
247
|
+
return message;
|
|
248
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extension,
|
|
3
|
+
isPacked,
|
|
4
|
+
pathFromKeys,
|
|
5
|
+
trailingSlash,
|
|
6
|
+
TraverseError,
|
|
7
|
+
Tree,
|
|
8
|
+
} from "@weborigami/async-tree";
|
|
9
|
+
import { typos } from "./typos.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Try to provide a more helpful message for a TraverseError by analyzing the
|
|
13
|
+
* code and suggesting possible typos.
|
|
14
|
+
*
|
|
15
|
+
* @param {TraverseError} error
|
|
16
|
+
*/
|
|
17
|
+
export default async function explainTraverseError(error) {
|
|
18
|
+
const { lastValue, keys, position } = error;
|
|
19
|
+
if (lastValue === undefined || keys === undefined || position === undefined) {
|
|
20
|
+
// Don't have sufficient information; shouldn't happen
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (position === 0) {
|
|
25
|
+
// Shouldn't happen; should have been a ReferenceError
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The key that caused the error is the one before the current position
|
|
30
|
+
const path = pathFromKeys(keys.slice(0, position));
|
|
31
|
+
let message = `The path traversal ended unexpectedly at: ${path}`;
|
|
32
|
+
|
|
33
|
+
const key = trailingSlash.remove(keys[position - 1]);
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
error.message === "A path tried to unpack data that's already unpacked."
|
|
37
|
+
) {
|
|
38
|
+
// Key ends in a slash but value isn't packed
|
|
39
|
+
message += `\nYou can drop the trailing slash and just use: ${key}`;
|
|
40
|
+
} else if (isPacked(lastValue) && typeof lastValue.unpack !== "function") {
|
|
41
|
+
// Missing an extension handler
|
|
42
|
+
const ext = extension.extname(key);
|
|
43
|
+
if (ext) {
|
|
44
|
+
message += `\nThe value couldn't be unpacked because no file extension handler is registered for "${ext}".`;
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
// Compare the requested key with the available keys
|
|
48
|
+
const lastValueKeys = await Tree.keys(lastValue);
|
|
49
|
+
const normalizedKeys = lastValueKeys.map(trailingSlash.remove);
|
|
50
|
+
|
|
51
|
+
const keyAsNumber = Number(key);
|
|
52
|
+
if (!isNaN(keyAsNumber)) {
|
|
53
|
+
// See if the string version of the key is present
|
|
54
|
+
if (lastValueKeys.includes(keyAsNumber)) {
|
|
55
|
+
const suggestedPath = `${pathFromKeys(keys.slice(0, position - 1))}(${keyAsNumber})`;
|
|
56
|
+
message += `\nSlash-separated keys are searched as strings. Here there's no string "${key}" key, but there is a number ${keyAsNumber} key.
|
|
57
|
+
To get the value for that number key, use parentheses: ${suggestedPath}`;
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
// Suggest typos
|
|
61
|
+
const possibleTypos = typos(key, normalizedKeys);
|
|
62
|
+
if (possibleTypos.length > 0) {
|
|
63
|
+
message += "\nPerhaps you intended";
|
|
64
|
+
if (possibleTypos.length > 1) {
|
|
65
|
+
message += " one of these";
|
|
66
|
+
}
|
|
67
|
+
message += ": ";
|
|
68
|
+
|
|
69
|
+
const withLeadingSlashes =
|
|
70
|
+
position > 1 ? possibleTypos.map(trailingSlash.add) : possibleTypos;
|
|
71
|
+
message += withLeadingSlashes.join(", ");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return message;
|
|
77
|
+
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import execute from "./execute.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Given parsed Origami code, return a function that executes that code.
|
|
5
5
|
*
|
|
6
|
-
* @typedef {import("
|
|
6
|
+
* @typedef {import("../../index.ts").RuntimeState} RuntimeState
|
|
7
|
+
* @typedef {import("../../index.js").AnnotatedCode} AnnotatedCode
|
|
7
8
|
*
|
|
8
|
-
* @param {
|
|
9
|
-
* @param {
|
|
9
|
+
* @param {AnnotatedCode} code - parsed Origami expression
|
|
10
|
+
* @param {RuntimeState} [state] - runtime state
|
|
10
11
|
*/
|
|
11
|
-
export function createExpressionFunction(code,
|
|
12
|
+
export function createExpressionFunction(code, state) {
|
|
12
13
|
async function fn() {
|
|
13
|
-
return
|
|
14
|
+
return execute(code, state);
|
|
14
15
|
}
|
|
15
16
|
fn.code = code;
|
|
16
17
|
fn.toString = () => code.location.source.text;
|
|
@@ -22,7 +23,7 @@ export function createExpressionFunction(code, parent) {
|
|
|
22
23
|
* expression.
|
|
23
24
|
*
|
|
24
25
|
* @param {any} obj
|
|
25
|
-
* @returns {obj is
|
|
26
|
+
* @returns {obj is AnnotatedCode}
|
|
26
27
|
*/
|
|
27
28
|
export function isExpressionFunction(obj) {
|
|
28
29
|
return typeof obj === "function" && obj.code;
|
|
@@ -6,8 +6,9 @@ import {
|
|
|
6
6
|
trailingSlash,
|
|
7
7
|
Tree,
|
|
8
8
|
} from "@weborigami/async-tree";
|
|
9
|
+
import execute from "./execute.js";
|
|
9
10
|
import handleExtension from "./handleExtension.js";
|
|
10
|
-
import {
|
|
11
|
+
import { ops } from "./internal.js";
|
|
11
12
|
|
|
12
13
|
export const KEY_TYPE = {
|
|
13
14
|
STRING: 0, // Simple string key: `a: 1`
|
|
@@ -65,7 +66,7 @@ export default async function expressionObject(entries, state = {}) {
|
|
|
65
66
|
for (const info of infos) {
|
|
66
67
|
if (info.keyType === KEY_TYPE.COMPUTED) {
|
|
67
68
|
const newState = Object.assign({}, state, { object: map });
|
|
68
|
-
const key = await
|
|
69
|
+
const key = await execute(/** @type {any} */ (info.key), newState);
|
|
69
70
|
// Destructively update the property info with the computed key
|
|
70
71
|
info.key = key;
|
|
71
72
|
defineProperty(object, info, state, map);
|
|
@@ -127,7 +128,7 @@ function defineProperty(object, propertyInfo, state, map) {
|
|
|
127
128
|
enumerable,
|
|
128
129
|
get: async () => {
|
|
129
130
|
const newState = Object.assign({}, state, { object: map });
|
|
130
|
-
const result = await
|
|
131
|
+
const result = await execute(value, newState);
|
|
131
132
|
return hasExtension ? handleExtension(result, key, map) : result;
|
|
132
133
|
},
|
|
133
134
|
});
|
|
@@ -215,11 +216,13 @@ export function propertyInfo(key, value) {
|
|
|
215
216
|
}
|
|
216
217
|
}
|
|
217
218
|
|
|
218
|
-
// Special case: a key with an extension has to be a getter
|
|
219
219
|
const hasExtension =
|
|
220
220
|
typeof key === "string" && extension.extname(key).length > 0;
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
|
|
222
|
+
// Special case: if the key has an extension but the value is a primitive,
|
|
223
|
+
// treat it as eager so we can handle the extension.
|
|
224
|
+
if (hasExtension && valueType === VALUE_TYPE.PRIMITIVE) {
|
|
225
|
+
valueType = VALUE_TYPE.EAGER;
|
|
223
226
|
}
|
|
224
227
|
|
|
225
228
|
return { enumerable, hasExtension, key, keyType, value, valueType };
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
setParent,
|
|
8
8
|
trailingSlash,
|
|
9
9
|
} from "@weborigami/async-tree";
|
|
10
|
+
import getPackedPath from "../handlers/getPackedPath.js";
|
|
10
11
|
import projectGlobals from "../project/projectGlobals.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -50,17 +51,30 @@ export default async function handleExtension(value, key, parent = null) {
|
|
|
50
51
|
setParent(value, parent);
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// Wrap the unpack function so its only called once per value.
|
|
56
|
-
let loadPromise;
|
|
57
|
-
value.unpack = async () => {
|
|
58
|
-
loadPromise ??= unpack(value, { key, parent });
|
|
59
|
-
return loadPromise;
|
|
60
|
-
};
|
|
54
|
+
if (handler.unpack) {
|
|
55
|
+
value.unpack = wrapUnpack(handler.unpack, value, key, parent);
|
|
61
56
|
}
|
|
62
57
|
}
|
|
63
58
|
}
|
|
64
59
|
}
|
|
60
|
+
value;
|
|
65
61
|
return value;
|
|
66
62
|
}
|
|
63
|
+
|
|
64
|
+
// Wrap the unpack function so it's only called once per value, and so we can
|
|
65
|
+
// add the file path to any errors it throws.
|
|
66
|
+
function wrapUnpack(unpack, value, key, parent) {
|
|
67
|
+
let result;
|
|
68
|
+
return async () => {
|
|
69
|
+
if (!result) {
|
|
70
|
+
try {
|
|
71
|
+
result = await unpack(value, { key, parent });
|
|
72
|
+
} catch (/** @type {any} */ error) {
|
|
73
|
+
const filePath = getPackedPath(value, { key, parent });
|
|
74
|
+
const message = `Can't unpack ${filePath}\n${error.message}`;
|
|
75
|
+
throw new error.constructor(message, { cause: error });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
};
|
|
80
|
+
}
|
package/src/runtime/internal.js
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { interop } from "@weborigami/async-tree";
|
|
2
|
+
import asyncStorage from "./asyncStorage.js";
|
|
3
|
+
import { lineInfo } from "./errors.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Inject our warning function into async-tree calls
|
|
7
|
+
*/
|
|
8
|
+
interop.warn = function warn(...args) {
|
|
9
|
+
console.warn(...args);
|
|
10
|
+
const context = asyncStorage.getStore();
|
|
11
|
+
const location = context?.code?.location;
|
|
12
|
+
if (location) {
|
|
13
|
+
console.warn(lineInfo(location));
|
|
14
|
+
}
|
|
15
|
+
};
|
package/src/runtime/ops.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import { getParent, isUnpackable, Tree } from "@weborigami/async-tree";
|
|
10
10
|
import os from "node:os";
|
|
11
|
+
import execute from "./execute.js";
|
|
11
12
|
import expressionObject from "./expressionObject.js";
|
|
12
|
-
import { evaluate } from "./internal.js";
|
|
13
13
|
import mergeTrees from "./mergeTrees.js";
|
|
14
14
|
import OrigamiFileMap from "./OrigamiFileMap.js";
|
|
15
15
|
import { codeSymbol } from "./symbols.js";
|
|
@@ -70,7 +70,7 @@ export async function cache(cache, path, code) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
// Don't await: might get another request for this before promise resolves
|
|
73
|
-
const promise = await
|
|
73
|
+
const promise = await execute(code);
|
|
74
74
|
|
|
75
75
|
// Save promise so another request will get the same promise
|
|
76
76
|
cache[path] = promise;
|
|
@@ -94,23 +94,13 @@ cache.unevaluatedArgs = true;
|
|
|
94
94
|
export async function comma(...args) {
|
|
95
95
|
let result;
|
|
96
96
|
for (const arg of args) {
|
|
97
|
-
result = await
|
|
97
|
+
result = await execute(arg);
|
|
98
98
|
}
|
|
99
99
|
return result;
|
|
100
100
|
}
|
|
101
101
|
addOpLabel(comma, "«ops.comma»");
|
|
102
102
|
comma.unevaluatedArgs = true;
|
|
103
103
|
|
|
104
|
-
/**
|
|
105
|
-
* Concatenate the given arguments.
|
|
106
|
-
*
|
|
107
|
-
* @param {any[]} args
|
|
108
|
-
*/
|
|
109
|
-
export async function concat(...args) {
|
|
110
|
-
return Tree.deepText(args);
|
|
111
|
-
}
|
|
112
|
-
addOpLabel(concat, "«ops.concat»");
|
|
113
|
-
|
|
114
104
|
export async function conditional(condition, truthy, falsy) {
|
|
115
105
|
const value = condition ? truthy : falsy;
|
|
116
106
|
return value instanceof Function ? await value() : value;
|
|
@@ -123,6 +113,16 @@ export async function construct(constructor, ...args) {
|
|
|
123
113
|
return Reflect.construct(constructor, args);
|
|
124
114
|
}
|
|
125
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Return the deep text of the arguments
|
|
118
|
+
*
|
|
119
|
+
* @param {any[]} args
|
|
120
|
+
*/
|
|
121
|
+
export async function deepText(...args) {
|
|
122
|
+
return Tree.deepText(args);
|
|
123
|
+
}
|
|
124
|
+
addOpLabel(deepText, "«ops.deepText");
|
|
125
|
+
|
|
126
126
|
/**
|
|
127
127
|
* Default value for a parameter: if the value is defined, return that;
|
|
128
128
|
* otherwise, return the result of invoking the initializer.
|
|
@@ -160,8 +160,8 @@ export async function flat(...args) {
|
|
|
160
160
|
args.map(async (arg) =>
|
|
161
161
|
arg instanceof Array || typeof arg !== "object"
|
|
162
162
|
? arg
|
|
163
|
-
: await Tree.values(arg)
|
|
164
|
-
)
|
|
163
|
+
: await Tree.values(arg),
|
|
164
|
+
),
|
|
165
165
|
);
|
|
166
166
|
|
|
167
167
|
return arrays.flat();
|
|
@@ -210,7 +210,7 @@ export async function inherited(depth, state) {
|
|
|
210
210
|
for (let i = 0; i < depth; i++) {
|
|
211
211
|
if (!current) {
|
|
212
212
|
throw new ReferenceError(
|
|
213
|
-
`Origami internal error: Can't find context object
|
|
213
|
+
`Origami internal error: Can't find context object`,
|
|
214
214
|
);
|
|
215
215
|
}
|
|
216
216
|
current = getParent(current);
|
|
@@ -255,7 +255,7 @@ export function lambda(length, parameters, code, state = {}) {
|
|
|
255
255
|
newState = Object.assign({}, state, { stack: newStack });
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
const result = await
|
|
258
|
+
const result = await execute(code, newState);
|
|
259
259
|
return result;
|
|
260
260
|
}
|
|
261
261
|
|
|
@@ -460,7 +460,12 @@ addOpLabel(optional, "«ops.optional»");
|
|
|
460
460
|
*/
|
|
461
461
|
export async function property(object, key) {
|
|
462
462
|
if (object == null) {
|
|
463
|
-
|
|
463
|
+
/** @type {any} */
|
|
464
|
+
const error = new ReferenceError(
|
|
465
|
+
"Tried to get a property of something that doesn't exist.",
|
|
466
|
+
);
|
|
467
|
+
error.position = 1; // position of the bad argument
|
|
468
|
+
throw error;
|
|
464
469
|
}
|
|
465
470
|
|
|
466
471
|
if (isUnpackable(object)) {
|
|
@@ -511,7 +516,7 @@ addOpLabel(rootDirectory, "«ops.rootDirectory»");
|
|
|
511
516
|
export async function scope(parent) {
|
|
512
517
|
if (!parent) {
|
|
513
518
|
throw new ReferenceError(
|
|
514
|
-
"Tried to find a value in scope, but no container was provided as the parent."
|
|
519
|
+
"Tried to find a value in scope, but no container was provided as the parent.",
|
|
515
520
|
);
|
|
516
521
|
}
|
|
517
522
|
return Tree.scope(parent);
|
package/src/runtime/symbols.js
CHANGED
package/src/runtime/typos.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Returns true if
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Returns true if one string could be a typo of the other.
|
|
3
|
+
*
|
|
4
|
+
* We generally define a typo as two strings with a Damerau-Levenshtein distance
|
|
5
|
+
* of 1. This will be true if the strings differ by a single insertion,
|
|
6
|
+
* deletion, substitution, or transposition.
|
|
7
|
+
*
|
|
8
|
+
* Additionally, we consider two strings that differ only in case to be typos,
|
|
9
|
+
* as well as two strings that differ only by accents.
|
|
5
10
|
*
|
|
6
11
|
* @param {string} s1
|
|
7
12
|
* @param {string} s2
|
|
@@ -15,6 +20,16 @@ export function isTypo(s1, s2) {
|
|
|
15
20
|
return false;
|
|
16
21
|
}
|
|
17
22
|
|
|
23
|
+
// If the strings are the same ignoring case, consider them typos
|
|
24
|
+
if (s1.toLowerCase() === s2.toLowerCase()) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// If the strings are the same ignoring accents, consider them typos
|
|
29
|
+
if (normalize(s1) === normalize(s2)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
18
33
|
// If strings are both a single character, we don't want to consider them
|
|
19
34
|
// typos.
|
|
20
35
|
if (length1 === 1 && length2 === 1) {
|
|
@@ -66,6 +81,10 @@ export function isTypo(s1, s2) {
|
|
|
66
81
|
return shorter === longer.slice(0, shorter.length);
|
|
67
82
|
}
|
|
68
83
|
|
|
84
|
+
function normalize(str) {
|
|
85
|
+
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
86
|
+
}
|
|
87
|
+
|
|
69
88
|
/**
|
|
70
89
|
* Return any strings that could be a typo of s
|
|
71
90
|
*
|