@weborigami/language 0.0.73 → 0.1.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/main.js +2 -2
- package/package.json +3 -3
- package/src/compiler/compile.js +10 -3
- package/src/compiler/origami.pegjs +127 -90
- package/src/compiler/parse.js +686 -688
- package/src/compiler/parserHelpers.js +13 -7
- 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 +58 -169
- package/src/runtime/typos.js +71 -0
- package/test/compiler/compile.test.js +4 -4
- package/test/compiler/parse.test.js +273 -145
- 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 +11 -12
- package/test/runtime/typos.test.js +21 -0
- package/src/runtime/formatError.js +0 -56
|
@@ -1,4 +1,5 @@
|
|
|
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
|
|
@@ -8,15 +9,21 @@ import * as ops from "../runtime/ops.js";
|
|
|
8
9
|
* location of the source code that produced it for debugging and error messages.
|
|
9
10
|
*/
|
|
10
11
|
export function annotate(parseResult, location) {
|
|
11
|
-
if (typeof parseResult === "object" && parseResult !== null) {
|
|
12
|
+
if (typeof parseResult === "object" && parseResult !== null && location) {
|
|
12
13
|
parseResult.location = location;
|
|
14
|
+
parseResult.source = codeFragment(location);
|
|
13
15
|
}
|
|
14
16
|
return parseResult;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* The indicated code is being used to define a property named by the given key.
|
|
21
|
+
* Rewrite any [ops.scope, key] calls to be [ops.inherited, key] to avoid
|
|
22
|
+
* infinite recursion.
|
|
23
|
+
*
|
|
24
|
+
* @param {import("../../index.ts").Code} code
|
|
25
|
+
* @param {string} key
|
|
26
|
+
*/
|
|
20
27
|
function avoidRecursivePropertyCalls(code, key) {
|
|
21
28
|
if (!(code instanceof Array)) {
|
|
22
29
|
return code;
|
|
@@ -35,8 +42,7 @@ function avoidRecursivePropertyCalls(code, key) {
|
|
|
35
42
|
// Process any nested code
|
|
36
43
|
modified = code.map((value) => avoidRecursivePropertyCalls(value, key));
|
|
37
44
|
}
|
|
38
|
-
|
|
39
|
-
modified.location = code.location;
|
|
45
|
+
annotate(modified, code.location);
|
|
40
46
|
return modified;
|
|
41
47
|
}
|
|
42
48
|
|
|
@@ -133,7 +139,7 @@ export function makeFunctionCall(target, chain, location) {
|
|
|
133
139
|
}
|
|
134
140
|
}
|
|
135
141
|
|
|
136
|
-
fnCall
|
|
142
|
+
annotate(fnCall, { start, source, end });
|
|
137
143
|
|
|
138
144
|
value = fnCall;
|
|
139
145
|
}
|
|
@@ -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
|
|
package/src/runtime/ops.js
CHANGED
|
@@ -1,30 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
3
3
|
* @typedef {import("@weborigami/async-tree").PlainObject} PlainObject
|
|
4
|
+
* @typedef {import("@weborigami/async-tree").Treelike} Treelike
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import {
|
|
7
|
-
ExplorableSiteTree,
|
|
8
8
|
ObjectTree,
|
|
9
|
-
SiteTree,
|
|
10
9
|
Tree,
|
|
11
10
|
isUnpackable,
|
|
12
|
-
pathFromKeys,
|
|
13
11
|
scope as scopeFn,
|
|
14
|
-
trailingSlash,
|
|
15
12
|
concat as treeConcat,
|
|
16
13
|
} from "@weborigami/async-tree";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import { builtinReferenceError, scopeReferenceError } from "./errors.js";
|
|
17
16
|
import expressionObject from "./expressionObject.js";
|
|
18
|
-
import { handleExtension } from "./extensions.js";
|
|
19
|
-
import HandleExtensionsTransform from "./HandleExtensionsTransform.js";
|
|
20
17
|
import { evaluate } from "./internal.js";
|
|
21
18
|
import mergeTrees from "./mergeTrees.js";
|
|
22
19
|
import OrigamiFiles from "./OrigamiFiles.js";
|
|
20
|
+
import { codeSymbol } from "./symbols.js";
|
|
23
21
|
import taggedTemplate from "./taggedTemplate.js";
|
|
24
22
|
|
|
25
|
-
// For memoizing lambda functions
|
|
26
|
-
const lambdaFnMap = new Map();
|
|
27
|
-
|
|
28
23
|
function addOpLabel(op, label) {
|
|
29
24
|
Object.defineProperty(op, "toString", {
|
|
30
25
|
value: () => label,
|
|
@@ -43,6 +38,29 @@ export async function array(...items) {
|
|
|
43
38
|
}
|
|
44
39
|
addOpLabel(array, "«ops.array»");
|
|
45
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Like ops.scope, but only searches for a builtin at the top of the scope
|
|
43
|
+
* chain.
|
|
44
|
+
*
|
|
45
|
+
* @this {AsyncTree|null}
|
|
46
|
+
*/
|
|
47
|
+
export async function builtin(key) {
|
|
48
|
+
if (!this) {
|
|
49
|
+
throw new Error("Tried to get the scope of a null or undefined tree.");
|
|
50
|
+
}
|
|
51
|
+
let current = this;
|
|
52
|
+
while (current.parent) {
|
|
53
|
+
current = current.parent;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const value = await current.get(key);
|
|
57
|
+
if (value === undefined) {
|
|
58
|
+
throw await builtinReferenceError(this, current, key);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
|
|
46
64
|
/**
|
|
47
65
|
* Look up the given key in the scope for the current tree the first time
|
|
48
66
|
* the key is requested, holding on to the value for future requests.
|
|
@@ -73,106 +91,6 @@ export async function concat(...args) {
|
|
|
73
91
|
}
|
|
74
92
|
addOpLabel(concat, "«ops.concat»");
|
|
75
93
|
|
|
76
|
-
/**
|
|
77
|
-
* Find the indicated constructor in scope, then return a function which invokes
|
|
78
|
-
* it with `new`.
|
|
79
|
-
*
|
|
80
|
-
* @this {AsyncTree}
|
|
81
|
-
* @param {...any} keys
|
|
82
|
-
*/
|
|
83
|
-
export async function constructor(...keys) {
|
|
84
|
-
const tree = this;
|
|
85
|
-
const scope = scopeFn(tree);
|
|
86
|
-
let constructor = await Tree.traverseOrThrow(scope, ...keys);
|
|
87
|
-
if (isUnpackable(constructor)) {
|
|
88
|
-
constructor = await constructor.unpack();
|
|
89
|
-
}
|
|
90
|
-
// Origami may pass `undefined` as the first argument to the constructor. We
|
|
91
|
-
// don't pass that along, because constructors like `Date` don't like it.
|
|
92
|
-
return (...args) =>
|
|
93
|
-
args.length === 1 && args[0] === undefined
|
|
94
|
-
? new constructor()
|
|
95
|
-
: new constructor(...args);
|
|
96
|
-
}
|
|
97
|
-
addOpLabel(constructor, "«ops.constructor»");
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Given a protocol, a host, and a list of keys, construct an href.
|
|
101
|
-
*
|
|
102
|
-
* @param {string} protocol
|
|
103
|
-
* @param {string} host
|
|
104
|
-
* @param {string[]} keys
|
|
105
|
-
*/
|
|
106
|
-
function constructHref(protocol, host, ...keys) {
|
|
107
|
-
const path = pathFromKeys(keys);
|
|
108
|
-
let href = [host, path].join("/");
|
|
109
|
-
if (!href.startsWith(protocol)) {
|
|
110
|
-
if (!href.startsWith("//")) {
|
|
111
|
-
href = `//${href}`;
|
|
112
|
-
}
|
|
113
|
-
href = `${protocol}${href}`;
|
|
114
|
-
}
|
|
115
|
-
return href;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Given a protocol, a host, and a list of keys, construct an href.
|
|
120
|
-
*
|
|
121
|
-
* @param {string} protocol
|
|
122
|
-
* @param {import("../../index.ts").Constructor<AsyncTree>} treeClass
|
|
123
|
-
* @param {AsyncTree|null} parent
|
|
124
|
-
* @param {string} host
|
|
125
|
-
* @param {string[]} keys
|
|
126
|
-
*/
|
|
127
|
-
async function constructSiteTree(protocol, treeClass, parent, host, ...keys) {
|
|
128
|
-
// If the last key doesn't end in a slash, remove it for now.
|
|
129
|
-
let lastKey;
|
|
130
|
-
if (keys.length > 0 && keys.at(-1) && !trailingSlash.has(keys.at(-1))) {
|
|
131
|
-
lastKey = keys.pop();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const href = constructHref(protocol, host, ...keys);
|
|
135
|
-
let result = new (HandleExtensionsTransform(treeClass))(href);
|
|
136
|
-
result.parent = parent;
|
|
137
|
-
|
|
138
|
-
return lastKey ? result.get(lastKey) : result;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* A site tree with JSON Keys via HTTPS.
|
|
143
|
-
*
|
|
144
|
-
* @this {AsyncTree|null}
|
|
145
|
-
* @param {string} host
|
|
146
|
-
* @param {...string} keys
|
|
147
|
-
*/
|
|
148
|
-
export function explorableSite(host, ...keys) {
|
|
149
|
-
return constructSiteTree("https:", ExplorableSiteTree, this, host, ...keys);
|
|
150
|
-
}
|
|
151
|
-
addOpLabel(explorableSite, "«ops.explorableSite»");
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Fetch the resource at the given href.
|
|
155
|
-
*
|
|
156
|
-
* @this {AsyncTree|null}
|
|
157
|
-
* @param {string} href
|
|
158
|
-
*/
|
|
159
|
-
async function fetchResponse(href) {
|
|
160
|
-
const response = await fetch(href);
|
|
161
|
-
if (!response.ok) {
|
|
162
|
-
return undefined;
|
|
163
|
-
}
|
|
164
|
-
let buffer = await response.arrayBuffer();
|
|
165
|
-
|
|
166
|
-
// Attach any loader defined for the file type.
|
|
167
|
-
const url = new URL(href);
|
|
168
|
-
const filename = url.pathname.split("/").pop();
|
|
169
|
-
if (this && filename) {
|
|
170
|
-
buffer = await handleExtension(this, buffer, filename);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return buffer;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
94
|
/**
|
|
177
95
|
* This op is only used during parsing. It signals to ops.object that the
|
|
178
96
|
* "arguments" of the expression should be used to define a property getter.
|
|
@@ -180,46 +98,26 @@ async function fetchResponse(href) {
|
|
|
180
98
|
export const getter = new String("«ops.getter»");
|
|
181
99
|
|
|
182
100
|
/**
|
|
183
|
-
*
|
|
101
|
+
* Files tree for the filesystem root.
|
|
184
102
|
*
|
|
185
103
|
* @this {AsyncTree|null}
|
|
186
104
|
*/
|
|
187
105
|
export async function filesRoot() {
|
|
188
|
-
let
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// (e.g., Origami expressions loaded from .ori files) will have access to
|
|
192
|
-
// things like the built-in functions.
|
|
193
|
-
root.parent = this;
|
|
194
|
-
|
|
195
|
-
return root;
|
|
106
|
+
let tree = new OrigamiFiles("/");
|
|
107
|
+
tree.parent = root(this);
|
|
108
|
+
return tree;
|
|
196
109
|
}
|
|
197
110
|
|
|
198
111
|
/**
|
|
199
|
-
*
|
|
112
|
+
* Files tree for the user's home directory.
|
|
200
113
|
*
|
|
201
114
|
* @this {AsyncTree|null}
|
|
202
|
-
* @param {string} host
|
|
203
|
-
* @param {...string} keys
|
|
204
115
|
*/
|
|
205
|
-
export async function
|
|
206
|
-
const
|
|
207
|
-
|
|
116
|
+
export async function homeTree() {
|
|
117
|
+
const tree = new OrigamiFiles(os.homedir());
|
|
118
|
+
tree.parent = root(this);
|
|
119
|
+
return tree;
|
|
208
120
|
}
|
|
209
|
-
addOpLabel(http, "«ops.http»");
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Retrieve a web resource via HTTPS.
|
|
213
|
-
*
|
|
214
|
-
* @this {AsyncTree|null}
|
|
215
|
-
* @param {string} host
|
|
216
|
-
* @param {...string} keys
|
|
217
|
-
*/
|
|
218
|
-
export function https(host, ...keys) {
|
|
219
|
-
const href = constructHref("https:", host, ...keys);
|
|
220
|
-
return fetchResponse.call(this, href);
|
|
221
|
-
}
|
|
222
|
-
addOpLabel(https, "«ops.https»");
|
|
223
121
|
|
|
224
122
|
/**
|
|
225
123
|
* Search the parent's scope -- i.e., exclude the current tree -- for the given
|
|
@@ -247,19 +145,21 @@ addOpLabel(inherited, "«ops.inherited»");
|
|
|
247
145
|
*/
|
|
248
146
|
|
|
249
147
|
export function lambda(parameters, code) {
|
|
250
|
-
|
|
251
|
-
return lambdaFnMap.get(code);
|
|
252
|
-
}
|
|
148
|
+
const context = this;
|
|
253
149
|
|
|
254
|
-
/** @this {
|
|
150
|
+
/** @this {Treelike|null} */
|
|
255
151
|
async function invoke(...args) {
|
|
256
152
|
// Add arguments to scope.
|
|
257
153
|
const ambients = {};
|
|
258
154
|
for (const parameter of parameters) {
|
|
259
155
|
ambients[parameter] = args.shift();
|
|
260
156
|
}
|
|
157
|
+
Object.defineProperty(ambients, codeSymbol, {
|
|
158
|
+
value: code,
|
|
159
|
+
enumerable: false,
|
|
160
|
+
});
|
|
261
161
|
const ambientTree = new ObjectTree(ambients);
|
|
262
|
-
ambientTree.parent =
|
|
162
|
+
ambientTree.parent = context;
|
|
263
163
|
|
|
264
164
|
let result = await evaluate.call(ambientTree, code);
|
|
265
165
|
|
|
@@ -286,7 +186,6 @@ export function lambda(parameters, code) {
|
|
|
286
186
|
});
|
|
287
187
|
|
|
288
188
|
invoke.code = code;
|
|
289
|
-
lambdaFnMap.set(code, invoke);
|
|
290
189
|
return invoke;
|
|
291
190
|
}
|
|
292
191
|
addOpLabel(lambda, "«ops.lambda");
|
|
@@ -323,6 +222,16 @@ export async function object(...entries) {
|
|
|
323
222
|
}
|
|
324
223
|
addOpLabel(object, "«ops.object»");
|
|
325
224
|
|
|
225
|
+
// Return the root of the given tree. For an Origami tree, this gives us
|
|
226
|
+
// a way of acessing the builtins.
|
|
227
|
+
function root(tree) {
|
|
228
|
+
let current = tree;
|
|
229
|
+
while (current.parent) {
|
|
230
|
+
current = current.parent;
|
|
231
|
+
}
|
|
232
|
+
return current;
|
|
233
|
+
}
|
|
234
|
+
|
|
326
235
|
/**
|
|
327
236
|
* Look up the given key in the scope for the current tree.
|
|
328
237
|
*
|
|
@@ -333,7 +242,11 @@ export async function scope(key) {
|
|
|
333
242
|
throw new Error("Tried to get the scope of a null or undefined tree.");
|
|
334
243
|
}
|
|
335
244
|
const scope = scopeFn(this);
|
|
336
|
-
|
|
245
|
+
const value = await scope.get(key);
|
|
246
|
+
if (value === undefined) {
|
|
247
|
+
throw await scopeReferenceError(scope, key);
|
|
248
|
+
}
|
|
249
|
+
return value;
|
|
337
250
|
}
|
|
338
251
|
addOpLabel(scope, "«ops.scope»");
|
|
339
252
|
|
|
@@ -361,30 +274,6 @@ addOpLabel(template, "«ops.template»");
|
|
|
361
274
|
*/
|
|
362
275
|
export const traverse = Tree.traverseOrThrow;
|
|
363
276
|
|
|
364
|
-
/**
|
|
365
|
-
* A website tree via HTTP.
|
|
366
|
-
*
|
|
367
|
-
* @this {AsyncTree|null}
|
|
368
|
-
* @param {string} host
|
|
369
|
-
* @param {...string} keys
|
|
370
|
-
*/
|
|
371
|
-
export function treeHttp(host, ...keys) {
|
|
372
|
-
return constructSiteTree("http:", SiteTree, this, host, ...keys);
|
|
373
|
-
}
|
|
374
|
-
addOpLabel(treeHttp, "«ops.treeHttp»");
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* A website tree via HTTPS.
|
|
378
|
-
*
|
|
379
|
-
* @this {AsyncTree|null}
|
|
380
|
-
* @param {string} host
|
|
381
|
-
* @param {...string} keys
|
|
382
|
-
*/
|
|
383
|
-
export function treeHttps(host, ...keys) {
|
|
384
|
-
return constructSiteTree("https:", SiteTree, this, host, ...keys);
|
|
385
|
-
}
|
|
386
|
-
addOpLabel(treeHttps, "«ops.treeHttps»");
|
|
387
|
-
|
|
388
277
|
/**
|
|
389
278
|
* If the value is packed but has an unpack method, call it and return that as
|
|
390
279
|
* the result; otherwise, return the value as is.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true if the two strings have a Damerau-Levenshtein distance of 1.
|
|
3
|
+
* This will be true if the strings differ by a single insertion, deletion,
|
|
4
|
+
* substitution, or transposition.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} s1
|
|
7
|
+
* @param {string} s2
|
|
8
|
+
*/
|
|
9
|
+
export function isTypo(s1, s2) {
|
|
10
|
+
const length1 = s1.length;
|
|
11
|
+
const length2 = s2.length;
|
|
12
|
+
|
|
13
|
+
// If the strings are identical, distance is 0
|
|
14
|
+
if (s1 === s2) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// If length difference is more than 1, distance can't be 1
|
|
19
|
+
if (Math.abs(length1 - length2) > 1) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (length1 === length2) {
|
|
24
|
+
// Check for one substitution
|
|
25
|
+
let differences = 0;
|
|
26
|
+
for (let i = 0; i < length1; i++) {
|
|
27
|
+
if (s1[i] !== s2[i]) {
|
|
28
|
+
differences++;
|
|
29
|
+
if (differences > 1) {
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (differences === 1) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check for one transposition
|
|
39
|
+
for (let i = 0; i < length1 - 1; i++) {
|
|
40
|
+
if (s1[i] !== s2[i]) {
|
|
41
|
+
// Check if swapping s1[i] and s1[i+1] matches s2
|
|
42
|
+
if (s1[i] === s2[i + 1] && s1[i + 1] === s2[i]) {
|
|
43
|
+
return s1.slice(i + 2) === s2.slice(i + 2);
|
|
44
|
+
} else {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check for one insertion/deletion
|
|
52
|
+
const longer = length1 > length2 ? s1 : s2;
|
|
53
|
+
const shorter = length1 > length2 ? s2 : s1;
|
|
54
|
+
for (let i = 0; i < shorter.length; i++) {
|
|
55
|
+
if (shorter[i] !== longer[i]) {
|
|
56
|
+
// If we skip this character, do the rest match?
|
|
57
|
+
return shorter.slice(i) === longer.slice(i + 1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return shorter === longer.slice(0, shorter.length);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Return any strings that could be a typo of s
|
|
65
|
+
*
|
|
66
|
+
* @param {string} s
|
|
67
|
+
* @param {string[]} strings
|
|
68
|
+
*/
|
|
69
|
+
export function typos(s, strings) {
|
|
70
|
+
return strings.filter((str) => isTypo(s, str));
|
|
71
|
+
}
|