@weborigami/language 0.6.9 → 0.6.11
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 +16 -17
- 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 +9 -3
- package/src/project/jsGlobals.js +3 -12
- package/src/protocols/package.js +20 -11
- package/src/runtime/asyncStorage.js +7 -0
- package/src/runtime/codeFragment.js +4 -3
- package/src/runtime/errors.js +86 -128
- package/src/runtime/evaluate.js +8 -77
- package/src/runtime/execute.js +85 -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 +4 -3
- 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 +51 -21
- 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 +42 -39
- 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 +3 -3
- package/test/runtime/ops.test.js +42 -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
|
});
|
|
@@ -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";
|
|
@@ -26,6 +26,28 @@ export function addition(a, b) {
|
|
|
26
26
|
}
|
|
27
27
|
addOpLabel(addition, "«ops.addition»");
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Flatten the arguments and then apply the function.
|
|
31
|
+
* This is used to handle spreads in function calls.
|
|
32
|
+
*/
|
|
33
|
+
export async function apply(fn, args, state) {
|
|
34
|
+
// TODO: This is starting to recapitulate much of execute()
|
|
35
|
+
if (isUnpackable(fn)) {
|
|
36
|
+
fn = await fn.unpack();
|
|
37
|
+
}
|
|
38
|
+
if (fn.needsState) {
|
|
39
|
+
// The function is an op that wants the runtime state
|
|
40
|
+
args.push(state);
|
|
41
|
+
}
|
|
42
|
+
const result =
|
|
43
|
+
fn instanceof Function
|
|
44
|
+
? await fn(...args) // Invoke the function
|
|
45
|
+
: await Tree.traverseOrThrow(fn, ...args); // Traverse the map.
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
addOpLabel(apply, "«ops.apply»");
|
|
49
|
+
apply.needsState = true;
|
|
50
|
+
|
|
29
51
|
/**
|
|
30
52
|
* Construct an array.
|
|
31
53
|
*
|
|
@@ -70,7 +92,7 @@ export async function cache(cache, path, code) {
|
|
|
70
92
|
}
|
|
71
93
|
|
|
72
94
|
// Don't await: might get another request for this before promise resolves
|
|
73
|
-
const promise = await
|
|
95
|
+
const promise = await execute(code);
|
|
74
96
|
|
|
75
97
|
// Save promise so another request will get the same promise
|
|
76
98
|
cache[path] = promise;
|
|
@@ -94,23 +116,13 @@ cache.unevaluatedArgs = true;
|
|
|
94
116
|
export async function comma(...args) {
|
|
95
117
|
let result;
|
|
96
118
|
for (const arg of args) {
|
|
97
|
-
result = await
|
|
119
|
+
result = await execute(arg);
|
|
98
120
|
}
|
|
99
121
|
return result;
|
|
100
122
|
}
|
|
101
123
|
addOpLabel(comma, "«ops.comma»");
|
|
102
124
|
comma.unevaluatedArgs = true;
|
|
103
125
|
|
|
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
126
|
export async function conditional(condition, truthy, falsy) {
|
|
115
127
|
const value = condition ? truthy : falsy;
|
|
116
128
|
return value instanceof Function ? await value() : value;
|
|
@@ -123,6 +135,16 @@ export async function construct(constructor, ...args) {
|
|
|
123
135
|
return Reflect.construct(constructor, args);
|
|
124
136
|
}
|
|
125
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Return the deep text of the arguments
|
|
140
|
+
*
|
|
141
|
+
* @param {any[]} args
|
|
142
|
+
*/
|
|
143
|
+
export async function deepText(...args) {
|
|
144
|
+
return Tree.deepText(args);
|
|
145
|
+
}
|
|
146
|
+
addOpLabel(deepText, "«ops.deepText");
|
|
147
|
+
|
|
126
148
|
/**
|
|
127
149
|
* Default value for a parameter: if the value is defined, return that;
|
|
128
150
|
* otherwise, return the result of invoking the initializer.
|
|
@@ -157,11 +179,14 @@ addOpLabel(exponentiation, "«ops.exponentiation»");
|
|
|
157
179
|
*/
|
|
158
180
|
export async function flat(...args) {
|
|
159
181
|
const arrays = await Promise.all(
|
|
160
|
-
args.map(async (arg) =>
|
|
161
|
-
|
|
182
|
+
args.map(async (arg) => {
|
|
183
|
+
if (isUnpackable(arg)) {
|
|
184
|
+
arg = await arg.unpack();
|
|
185
|
+
}
|
|
186
|
+
return arg instanceof Array || typeof arg !== "object"
|
|
162
187
|
? arg
|
|
163
|
-
: await Tree.values(arg)
|
|
164
|
-
)
|
|
188
|
+
: await Tree.values(arg);
|
|
189
|
+
}),
|
|
165
190
|
);
|
|
166
191
|
|
|
167
192
|
return arrays.flat();
|
|
@@ -210,7 +235,7 @@ export async function inherited(depth, state) {
|
|
|
210
235
|
for (let i = 0; i < depth; i++) {
|
|
211
236
|
if (!current) {
|
|
212
237
|
throw new ReferenceError(
|
|
213
|
-
`Origami internal error: Can't find context object
|
|
238
|
+
`Origami internal error: Can't find context object`,
|
|
214
239
|
);
|
|
215
240
|
}
|
|
216
241
|
current = getParent(current);
|
|
@@ -255,7 +280,7 @@ export function lambda(length, parameters, code, state = {}) {
|
|
|
255
280
|
newState = Object.assign({}, state, { stack: newStack });
|
|
256
281
|
}
|
|
257
282
|
|
|
258
|
-
const result = await
|
|
283
|
+
const result = await execute(code, newState);
|
|
259
284
|
return result;
|
|
260
285
|
}
|
|
261
286
|
|
|
@@ -460,7 +485,12 @@ addOpLabel(optional, "«ops.optional»");
|
|
|
460
485
|
*/
|
|
461
486
|
export async function property(object, key) {
|
|
462
487
|
if (object == null) {
|
|
463
|
-
|
|
488
|
+
/** @type {any} */
|
|
489
|
+
const error = new ReferenceError(
|
|
490
|
+
"Tried to get a property of something that doesn't exist.",
|
|
491
|
+
);
|
|
492
|
+
error.position = 1; // position of the bad argument
|
|
493
|
+
throw error;
|
|
464
494
|
}
|
|
465
495
|
|
|
466
496
|
if (isUnpackable(object)) {
|
|
@@ -511,7 +541,7 @@ addOpLabel(rootDirectory, "«ops.rootDirectory»");
|
|
|
511
541
|
export async function scope(parent) {
|
|
512
542
|
if (!parent) {
|
|
513
543
|
throw new ReferenceError(
|
|
514
|
-
"Tried to find a value in scope, but no container was provided as the parent."
|
|
544
|
+
"Tried to find a value in scope, but no container was provided as the parent.",
|
|
515
545
|
);
|
|
516
546
|
}
|
|
517
547
|
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
|
*
|