run npm install to generate a package lock

This commit is contained in:
sashinexists
2024-12-07 13:18:31 +11:00
parent e7d08a91b5
commit 23437d228e
2501 changed files with 290663 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
import { Mixin } from "../../index.ts";
declare const EventTargetMixin: Mixin<{
addEventListener(type: string, listener: EventListener): void;
dispatchEvent(event: Event): boolean;
removeEventListener(type: string, listener: EventListener): void;
}>;
export default EventTargetMixin;

View File

@@ -0,0 +1,117 @@
const listenersKey = Symbol("listeners");
export default function EventTargetMixin(Base) {
// Based on https://github.com/piranna/EventTarget.js
return class EventTarget extends Base {
constructor(...args) {
super(...args);
this[listenersKey] = {};
}
addEventListener(type, callback) {
if (!callback) {
return;
}
let listenersOfType = this[listenersKey][type];
if (!listenersOfType) {
this[listenersKey][type] = [];
listenersOfType = this[listenersKey][type];
}
// Don't add the same callback twice.
if (listenersOfType.includes(callback)) {
return;
}
listenersOfType.push(callback);
}
dispatchEvent(event) {
if (!(event instanceof Event)) {
throw TypeError("Argument to dispatchEvent must be an Event");
}
let stopImmediatePropagation = false;
let defaultPrevented = false;
if (!event.cancelable) {
Object.defineProperty(event, "cancelable", {
value: true,
enumerable: true,
});
}
if (!event.defaultPrevented) {
Object.defineProperty(event, "defaultPrevented", {
get: () => defaultPrevented,
enumerable: true,
});
}
// 2023-09-11: Setting isTrusted causes exception on Glitch
// if (!event.isTrusted) {
// Object.defineProperty(event, "isTrusted", {
// value: false,
// enumerable: true,
// });
// }
if (!event.target) {
Object.defineProperty(event, "target", {
value: this,
enumerable: true,
});
}
if (!event.timeStamp) {
Object.defineProperty(event, "timeStamp", {
value: new Date().getTime(),
enumerable: true,
});
}
event.preventDefault = function () {
if (this.cancelable) {
defaultPrevented = true;
}
};
event.stopImmediatePropagation = function () {
stopImmediatePropagation = true;
};
event.stopPropagation = function () {
// This is a no-op because we don't support event bubbling.
};
const type = event.type;
const listenersOfType = this[listenersKey][type] || [];
for (const listener of listenersOfType) {
if (stopImmediatePropagation) {
break;
}
listener.call(this, event);
}
return !event.defaultPrevented;
}
removeEventListener(type, callback) {
if (!callback) {
return;
}
let listenersOfType = this[listenersKey][type];
if (!listenersOfType) {
return;
}
// Remove callback from listeners.
listenersOfType = listenersOfType.filter(
(listener) => listener !== callback
);
// If there are no more listeners for this type, remove the type.
if (listenersOfType.length === 0) {
delete this[listenersKey][type];
} else {
this[listenersKey][type] = listenersOfType;
}
}
};
}

View File

@@ -0,0 +1,5 @@
import { Mixin } from "../../index.ts";
declare const HandleExtensionsTransform: Mixin<{}>;
export default HandleExtensionsTransform;

View File

@@ -0,0 +1,17 @@
import { handleExtension } from "./handlers.js";
/**
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
* @typedef {import("../../index.ts").Constructor<AsyncTree>} AsyncTreeConstructor
* @typedef {import("../../index.ts").UnpackFunction} FileUnpackFunction
*
* @param {AsyncTreeConstructor} Base
*/
export default function HandleExtensionsTransform(Base) {
return class FileLoaders extends Base {
async get(key) {
const value = await super.get(key);
return handleExtension(this, value, key);
}
};
}

View File

@@ -0,0 +1,5 @@
import { Mixin } from "../../index.ts";
declare const ImportModulesMixin: Mixin<{}>;
export default ImportModulesMixin;

View File

@@ -0,0 +1,58 @@
import * as fs from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { maybeOrigamiSourceCode } from "./errors.js";
/**
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
* @typedef {import("../../index.ts").Constructor<AsyncTree & { dirname: string }>} BaseConstructor
* @param {BaseConstructor} Base
*/
export default function ImportModulesMixin(Base) {
return class ImportModules extends Base {
async import(...keys) {
const filePath = path.join(this.dirname, ...keys);
// On Windows, absolute paths must be valid file:// URLs.
const fileUrl = pathToFileURL(filePath);
const modulePath = fileUrl.href;
// Try to load the module.
let obj;
try {
obj = await import(modulePath);
} catch (/** @type {any} */ error) {
if (error.code !== "ERR_MODULE_NOT_FOUND") {
throw error;
}
// Does the module exist as a file?
try {
await fs.stat(filePath);
} catch (error) {
// File doesn't exist
return undefined;
}
// Module exists, but we can't load it. Is the error internal?
if (maybeOrigamiSourceCode(error.message)) {
throw new Error(
`Internal Origami error loading ${filePath}\n${error.message}`
);
}
// Error may be a syntax error, so we offer that as a hint.
const message = `Error loading ${filePath}, possibly due to a syntax error.\n${error.message}`;
throw new SyntaxError(message);
}
if ("default" in obj) {
// Module with a default export; return that.
return obj.default;
} else {
// Module with multiple named exports. Cast from a module namespace
// object to a plain object.
return { ...obj };
}
}
};
}

View File

@@ -0,0 +1,5 @@
import { Mixin } from "../../index.ts";
declare const InvokeFunctionsTransform: Mixin<{}>;
export default InvokeFunctionsTransform;

View File

@@ -0,0 +1,25 @@
import { Tree } from "@weborigami/async-tree";
/**
* When using `get` to retrieve a value from a tree, if the value is a
* function, invoke it and return the result.
*
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
* @typedef {import("../../index.js").Constructor<AsyncTree>} AsyncTreeConstructor
* @param {AsyncTreeConstructor} Base
*/
export default function InvokeFunctionsTransform(Base) {
return class InvokeFunctions extends Base {
async get(key) {
let value = await super.get(key);
if (typeof value === "function") {
value = await value.call(this);
if (Tree.isAsyncTree(value) && !value.parent) {
value.parent = this;
}
}
return value;
}
};
}

View File

@@ -0,0 +1,11 @@
import { FileTree } from "@weborigami/async-tree";
import EventTargetMixin from "./EventTargetMixin.js";
import HandleExtensionsTransform from "./HandleExtensionsTransform.js";
import ImportModulesMixin from "./ImportModulesMixin.js";
import WatchFilesMixin from "./WatchFilesMixin.js";
export default class OrigamiFiles extends HandleExtensionsTransform(
(
ImportModulesMixin(WatchFilesMixin(EventTargetMixin(FileTree)))
)
) {}

View File

@@ -0,0 +1,9 @@
import { FileTree } from "@weborigami/async-tree";
import EventTargetMixin from "./EventTargetMixin.js";
import HandleExtensionsTransform from "./HandleExtensionsTransform.js";
import ImportModulesMixin from "./ImportModulesMixin.js";
import WatchFilesMixin from "./WatchFilesMixin.js";
export default class OrigamiFiles extends HandleExtensionsTransform(
ImportModulesMixin(WatchFilesMixin(EventTargetMixin(FileTree)))
) {}

View File

@@ -0,0 +1 @@
Modules necessary to evaluate Origami expressions

View File

@@ -0,0 +1,6 @@
export default class TreeEvent extends Event {
constructor(type, options = {}) {
super(type, options);
this.options = options;
}
}

View File

@@ -0,0 +1,5 @@
import { Mixin } from "../../index.ts";
declare const WatchFilesMixin: Mixin<{}>;
export default WatchFilesMixin;

View File

@@ -0,0 +1,59 @@
import * as fs from "node:fs/promises";
import path from "node:path";
import Watcher from "watcher";
import TreeEvent from "./TreeEvent.js";
// Map of paths to trees used by watcher
const pathTreeMap = new Map();
export default function WatchFilesMixin(Base) {
return class WatchFiles extends Base {
addEventListener(type, listener) {
super.addEventListener(type, listener);
if (type === "change") {
this.watch();
}
}
onChange(key) {
// Reset cached values.
this.subfoldersMap = new Map();
this.dispatchEvent(new TreeEvent("change", { key }));
}
async unwatch() {
if (!this.watching) {
return;
}
this.watcher?.close();
this.watching = false;
}
// Turn on watching for the directory.
async watch() {
if (this.watching) {
return;
}
this.watching = true;
// Ensure the directory exists.
await fs.mkdir(this.dirname, { recursive: true });
this.watcher = new Watcher(this.dirname, {
ignoreInitial: true,
persistent: false,
recursive: true,
});
this.watcher.on("all", (event, filePath) => {
const key = path.basename(filePath);
this.onChange(key);
});
// Add to the list of FileTree instances watching this directory.
const treeRefs = pathTreeMap.get(this.dirname) ?? [];
treeRefs.push(new WeakRef(this));
pathTreeMap.set(this.dirname, treeRefs);
}
};
}

View File

@@ -0,0 +1,19 @@
export default function codeFragment(location) {
const { source, start, end } = location;
let fragment =
start.offset < end.offset
? source.text.slice(start.offset, end.offset)
: // Use entire source
source.text;
// Replace newlines and whitespace runs with a single space.
fragment = fragment.replace(/(\n|\s\s+)+/g, " ");
// If longer than 80 characters, truncate with an ellipsis.
if (fragment.length > 80) {
fragment = fragment.slice(0, 80) + "…";
}
return fragment;
}

104
node_modules/@weborigami/language/src/runtime/errors.js generated vendored Normal file
View File

@@ -0,0 +1,104 @@
// Text we look for in an error stack to guess whether a given line represents a
import { scope as scopeFn, trailingSlash } from "@weborigami/async-tree";
import codeFragment from "./codeFragment.js";
import { typos } from "./typos.js";
// function in the Origami source code.
const origamiSourceSignals = [
"async-tree/src/",
"language/src/",
"origami/src/",
"at Scope.evaluate",
];
export async function builtinReferenceError(tree, builtins, key) {
const messages = [
`"${key}" is being called as if it were a builtin function, but it's not.`,
];
// See if the key is in scope (but not as a builtin)
const scope = scopeFn(tree);
const value = await scope.get(key);
if (value === undefined) {
const typos = await formatScopeTypos(builtins, key);
messages.push(typos);
} else {
messages.push(`Use "${key}/" instead.`);
}
const message = messages.join(" ");
return new ReferenceError(message);
}
/**
* Format an error for display in the console.
*
* @param {Error} error
*/
export function formatError(error) {
let message;
if (error.stack) {
// Display the stack only until we reach the Origami source code.
message = "";
let lines = error.stack.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (maybeOrigamiSourceCode(line)) {
break;
}
if (message) {
message += "\n";
}
message += lines[i];
}
} else {
message = error.toString();
}
// Add location
let location = /** @type {any} */ (error).location;
if (location) {
const fragment = codeFragment(location);
let { source, start } = location;
message += `\nevaluating: ${fragment}`;
if (typeof source === "object" && source.url) {
message += `\n at ${source.url.href}:${start.line}:${start.column}`;
} else if (source.text.includes("\n")) {
message += `\n at line ${start.line}, column ${start.column}`;
}
}
return message;
}
export async function formatScopeTypos(scope, key) {
const keys = await scopeTypos(scope, key);
// Don't match deprecated keys
const filtered = keys.filter((key) => !key.startsWith("@"));
if (filtered.length === 0) {
return "";
}
const quoted = filtered.map((key) => `"${key}"`);
const list = quoted.join(", ");
return `Maybe you meant ${list}?`;
}
export function maybeOrigamiSourceCode(text) {
return origamiSourceSignals.some((signal) => text.includes(signal));
}
export async function scopeReferenceError(scope, key) {
const messages = [
`"${key}" is not in scope.`,
await formatScopeTypos(scope, key),
];
const message = messages.join(" ");
return new ReferenceError(message);
}
// Return all possible typos for `key` in scope
async function scopeTypos(scope, key) {
const scopeKeys = [...(await scope.keys())];
const normalizedScopeKeys = scopeKeys.map((key) => trailingSlash.remove(key));
const normalizedKey = trailingSlash.remove(key);
return typos(normalizedKey, normalizedScopeKeys);
}

View File

@@ -0,0 +1,116 @@
import { Tree, isUnpackable, scope } from "@weborigami/async-tree";
import codeFragment from "./codeFragment.js";
import { ops } from "./internal.js";
import { codeSymbol, scopeSymbol, sourceSymbol } from "./symbols.js";
/**
* Evaluate the given code and return the result.
*
* `this` should be the tree used as the context for the evaluation.
*
* @this {import("@weborigami/types").AsyncTree|null}
* @param {import("../../index.ts").Code} code
*/
export default async function evaluate(code) {
const tree = this;
if (!(code instanceof Array)) {
// Simple scalar; return as is.
return code;
}
let evaluated;
const unevaluatedFns = [ops.lambda, ops.object, ops.literal];
if (unevaluatedFns.includes(code[0])) {
// Don't evaluate instructions, use as is.
evaluated = code;
} else {
// Evaluate each instruction in the code.
evaluated = await Promise.all(
code.map((instruction) => evaluate.call(tree, instruction))
);
}
// The head of the array is a function or a tree; the rest are args or keys.
let [fn, ...args] = evaluated;
if (!fn) {
// The code wants to invoke something that's couldn't be found in scope.
const error = ReferenceError(
`${codeFragment(code[0].location)} is not defined`
);
/** @type {any} */ (error).location = code.location;
throw error;
}
if (isUnpackable(fn)) {
// Unpack the object and use the result as the function or tree.
fn = await fn.unpack();
}
if (!Tree.isTreelike(fn)) {
const text = fn.toString?.() ?? codeFragment(code[0].location);
const error = new TypeError(
`Not a callable function or tree: ${text.slice(0, 80)}`
);
/** @type {any} */ (error).location = code.location;
throw error;
}
// Execute the function or traverse the tree.
let result;
try {
result =
fn instanceof Function
? await fn.call(tree, ...args) // Invoke the function
: await Tree.traverseOrThrow(fn, ...args); // Traverse the tree.
} catch (/** @type {any} */ error) {
if (!error.location) {
// Attach the location of the code we tried to evaluate.
error.location =
error.position !== undefined && code[error.position + 1]?.location
? // Use location of the argument with the given position (need to
// offset by 1 to skip the function).
code[error.position + 1]?.location
: // Use overall location.
code.location;
}
throw error;
}
// If the result is a tree, then the default parent of the tree is the current
// tree.
if (Tree.isAsyncTree(result) && !result.parent) {
result.parent = tree;
}
// To aid debugging, add the code to the result.
if (Object.isExtensible(result)) {
try {
if (code.location && !result[sourceSymbol]) {
Object.defineProperty(result, sourceSymbol, {
value: codeFragment(code.location),
enumerable: false,
});
}
if (!result[codeSymbol]) {
Object.defineProperty(result, codeSymbol, {
value: code,
enumerable: false,
});
}
if (!result[scopeSymbol]) {
Object.defineProperty(result, scopeSymbol, {
get() {
return scope(this).trees;
},
enumerable: false,
});
}
} catch (/** @type {any} */ error) {
// Ignore errors.
}
}
return result;
}

View File

@@ -0,0 +1,33 @@
/** @typedef {import("@weborigami/types").AsyncTree} AsyncTree */
import { evaluate } from "./internal.js";
/**
* Given parsed Origami code, return a function that executes that code.
*
* @param {import("../../index.js").Code} code - parsed Origami expression
* @param {string} [name] - optional name of the function
*/
export function createExpressionFunction(code, name) {
/** @this {AsyncTree|null} */
async function fn() {
return evaluate.call(this, code);
}
if (name) {
Object.defineProperty(fn, "name", { value: name });
}
fn.code = code;
fn.toString = () => code.location.source.text;
return fn;
}
/**
* Return true if the given object is a function that executes an Origami
* expression.
*
* @param {any} obj
* @returns {obj is { code: Array }}
*/
export function isExpressionFunction(obj) {
return typeof obj === "function" && obj.code;
}

View File

@@ -0,0 +1,120 @@
import { extension, ObjectTree, symbols, Tree } from "@weborigami/async-tree";
import { handleExtension } from "./handlers.js";
import { evaluate, ops } from "./internal.js";
/**
* Given an array of entries with string keys and Origami code values (arrays of
* ops and operands), return an object with the same keys defining properties
* whose getters evaluate the code.
*
* The value can take three forms:
*
* 1. A primitive value (string, etc.). This will be defined directly as an
* object property.
* 1. An immediate code entry. This will be evaluated during this call and its
* result defined as an object property.
* 1. A code entry that starts with ops.getter. This will be defined as a
* property getter on the object.
*
* @param {*} entries
* @param {import("@weborigami/types").AsyncTree | null} parent
*/
export default async function expressionObject(entries, parent) {
// Create the object and set its parent
const object = {};
if (parent !== null && !Tree.isAsyncTree(parent)) {
throw new TypeError(`Parent must be an AsyncTree or null`);
}
Object.defineProperty(object, symbols.parent, {
configurable: true,
enumerable: false,
value: parent,
writable: true,
});
let tree;
const immediateProperties = [];
for (let [key, value] of entries) {
// Determine if we need to define a getter or a regular property. If the key
// has an extension, we need to define a getter. If the value is code (an
// array), we need to define a getter -- but if that code takes the form
// [ops.getter, <primitive>], we can define a regular property.
let defineProperty;
const extname = extension.extname(key);
if (extname) {
defineProperty = false;
} else if (!(value instanceof Array)) {
defineProperty = true;
} else if (value[0] === ops.getter && !(value[1] instanceof Array)) {
defineProperty = true;
value = value[1];
} else {
defineProperty = false;
}
// If the key is wrapped in parentheses, it is not enumerable.
let enumerable = true;
if (key[0] === "(" && key[key.length - 1] === ")") {
key = key.slice(1, -1);
enumerable = false;
}
if (defineProperty) {
// Define simple property
// object[key] = value;
Object.defineProperty(object, key, {
configurable: true,
enumerable,
value,
writable: true,
});
} else {
// Property getter
let code;
if (value[0] === ops.getter) {
code = value[1];
} else {
immediateProperties.push(key);
code = value;
}
let get;
if (extname) {
// Key has extension, getter will invoke code then attach unpack method
get = async () => {
tree ??= new ObjectTree(object);
const result = await evaluate.call(tree, code);
return handleExtension(tree, result, key);
};
} else {
// No extension, so getter just invokes code.
get = async () => {
tree ??= new ObjectTree(object);
return evaluate.call(tree, code);
};
}
Object.defineProperty(object, key, {
configurable: true,
enumerable,
get,
});
}
}
// Evaluate any properties that were declared as immediate: get their value
// and overwrite the property getter with the actual value.
for (const key of immediateProperties) {
const value = await object[key];
// @ts-ignore Unclear why TS thinks `object` might be undefined here
const enumerable = Object.getOwnPropertyDescriptor(object, key).enumerable;
Object.defineProperty(object, key, {
configurable: true,
enumerable,
value,
writable: true,
});
}
return object;
}

View File

@@ -0,0 +1,24 @@
import { map, Tree } from "@weborigami/async-tree";
/**
* When using `get` to retrieve a value from a tree, if the value is a
* function, invoke it and return the result.
*/
export default function functionResultsMap(treelike) {
return map(treelike, {
description: "functionResultsMap",
value: async (sourceValue, sourceKey, tree) => {
let resultValue;
if (typeof sourceValue === "function") {
resultValue = await sourceValue.call(tree);
if (Tree.isAsyncTree(resultValue) && !resultValue.parent) {
resultValue.parent = tree;
}
} else {
resultValue = sourceValue;
}
return resultValue;
},
});
}

View File

@@ -0,0 +1,110 @@
import {
box,
extension,
isPacked,
isStringLike,
isUnpackable,
scope,
symbols,
trailingSlash,
} from "@weborigami/async-tree";
/** @typedef {import("../../index.ts").ExtensionHandler} ExtensionHandler */
// Track extensions handlers for a given containing tree.
const handlersForContainer = new Map();
/**
* Find an extension handler for a file in the given container.
*
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
*
* @param {AsyncTree} parent
* @param {string} extension
*/
export async function getExtensionHandler(parent, extension) {
let handlers = handlersForContainer.get(parent);
if (handlers) {
if (handlers[extension]) {
return handlers[extension];
}
} else {
handlers = {};
handlersForContainer.set(parent, handlers);
}
const handlerName = `${extension.slice(1)}.handler`;
const parentScope = scope(parent);
/** @type {Promise<ExtensionHandler>} */
let handlerPromise = parentScope
?.get(handlerName)
.then(async (extensionHandler) => {
if (isUnpackable(extensionHandler)) {
// The extension handler itself needs to be unpacked. E.g., if it's a
// buffer containing JavaScript file, we need to unpack it to get its
// default export.
// @ts-ignore
extensionHandler = await extensionHandler.unpack();
}
// Update cache with actual handler
handlers[extension] = extensionHandler;
return extensionHandler;
});
// Cache handler even if it's undefined so we don't look it up again
handlers[extension] = handlerPromise;
return handlerPromise;
}
/**
* If the given value is packed (e.g., buffer) and the key is a string-like path
* that ends in an extension, search for a handler for that extension and, if
* found, attach it to the value.
*
* @param {import("@weborigami/types").AsyncTree} parent
* @param {any} value
* @param {any} key
*/
export async function handleExtension(parent, value, key) {
if (isPacked(value) && isStringLike(key) && value.unpack === undefined) {
const hasSlash = trailingSlash.has(key);
if (hasSlash) {
key = trailingSlash.remove(key);
}
// Special case: `.ori.<ext>` extensions are Origami documents.
const extname = key.match(/\.ori\.\S+$/)
? ".oridocument"
: extension.extname(key);
if (extname) {
const handler = await getExtensionHandler(parent, extname);
if (handler) {
if (hasSlash && handler.unpack) {
// Key like `data.json/` ends in slash -- unpack immediately
return handler.unpack(value, { key, parent });
}
// If the value is a primitive, box it so we can attach data to it.
value = box(value);
if (handler.mediaType) {
value.mediaType = handler.mediaType;
}
value[symbols.parent] = parent;
const unpack = handler.unpack;
if (unpack) {
// Wrap the unpack function so its only called once per value.
let loadPromise;
value.unpack = async () => {
loadPromise ??= unpack(value, { key, parent });
return loadPromise;
};
}
}
}
}
return value;
}

View File

@@ -0,0 +1,15 @@
//
// The runtime includes a number of modules with circular dependencies. This
// module exists to explicitly set the loading order for those modules. To
// enforce use of this loading order, other modules should only load the modules
// below via this module.
//
// About this pattern: https://medium.com/visual-development/how-to-fix-nasty-circular-dependency-issues-once-and-for-all-in-javascript-typescript-a04c987cf0de
//
// Note: to avoid having VS Code auto-sort the imports, keep lines between them.
export * as ops from "./ops.js";
export { default as evaluate } from "./evaluate.js";
export * as expressionFunction from "./expressionFunction.js";

View File

@@ -0,0 +1,43 @@
import { isPlainObject, isUnpackable, merge } from "@weborigami/async-tree";
/**
* Create a tree that's the result of merging the given trees.
*
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
* @typedef {import("@weborigami/async-tree").Treelike} Treelike
*
* @this {AsyncTree|null}
* @param {(Treelike|null)[]} trees
*/
export default async function mergeTrees(...trees) {
// Filter out null or undefined trees.
/** @type {Treelike[]}
* @ts-ignore */
const filtered = trees.filter((tree) => tree);
if (filtered.length === 1) {
// Only one tree, no need to merge.
return filtered[0];
}
// Unpack any packed objects.
const unpacked = await Promise.all(
filtered.map((obj) =>
isUnpackable(obj) ? /** @type {any} */ (obj).unpack() : obj
)
);
// If all trees are plain objects, return a plain object.
if (unpacked.every((tree) => isPlainObject(tree))) {
return Object.assign({}, ...unpacked);
}
// If all trees are arrays, return an array.
if (unpacked.every((tree) => Array.isArray(tree))) {
return unpacked.flat();
}
// Merge the trees.
const result = merge(...unpacked);
return result;
}

477
node_modules/@weborigami/language/src/runtime/ops.js generated vendored Normal file
View File

@@ -0,0 +1,477 @@
/**
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
* @typedef {import("@weborigami/async-tree").PlainObject} PlainObject
* @typedef {import("@weborigami/async-tree").Treelike} Treelike
*/
import {
ObjectTree,
Tree,
isUnpackable,
scope as scopeFn,
concat as treeConcat,
} from "@weborigami/async-tree";
import os from "node:os";
import { builtinReferenceError, scopeReferenceError } from "./errors.js";
import expressionObject from "./expressionObject.js";
import { evaluate } from "./internal.js";
import mergeTrees from "./mergeTrees.js";
import OrigamiFiles from "./OrigamiFiles.js";
import { codeSymbol } from "./symbols.js";
import taggedTemplate from "./taggedTemplate.js";
function addOpLabel(op, label) {
Object.defineProperty(op, "toString", {
value: () => label,
enumerable: false,
});
}
export function addition(a, b) {
return a + b;
}
addOpLabel(addition, "«ops.addition»");
export function bitwiseAnd(a, b) {
return a & b;
}
addOpLabel(bitwiseAnd, "«ops.bitwiseAnd»");
export function bitwiseNot(a) {
return ~a;
}
addOpLabel(bitwiseNot, "«ops.bitwiseNot»");
export function bitwiseOr(a, b) {
return a | b;
}
addOpLabel(bitwiseOr, "«ops.bitwiseOr»");
export function bitwiseXor(a, b) {
return a ^ b;
}
addOpLabel(bitwiseXor, "«ops.bitwiseXor»");
/**
* Construct an array.
*
* @this {AsyncTree|null}
* @param {any[]} items
*/
export async function array(...items) {
return items;
}
addOpLabel(array, "«ops.array»");
/**
* Like ops.scope, but only searches for a builtin at the top of the scope
* chain.
*
* @this {AsyncTree|null}
*/
export async function builtin(key) {
if (!this) {
throw new Error("Tried to get the scope of a null or undefined tree.");
}
const builtins = Tree.root(this);
const value = await builtins.get(key);
if (value === undefined) {
throw await builtinReferenceError(this, builtins, key);
}
return value;
}
/**
* JavaScript comma operator, returns the last argument.
*
* @param {...any} args
* @returns
*/
export function comma(...args) {
return args.at(-1);
}
addOpLabel(comma, "«ops.comma»");
/**
* Concatenate the given arguments.
*
* @this {AsyncTree|null}
* @param {any[]} args
*/
export async function concat(...args) {
return treeConcat.call(this, args);
}
addOpLabel(concat, "«ops.concat»");
export async function conditional(condition, truthy, falsy) {
return condition ? truthy() : falsy();
}
export function division(a, b) {
return a / b;
}
addOpLabel(division, "«ops.division»");
export function equal(a, b) {
return a == b;
}
addOpLabel(equal, "«ops.equal»");
export function exponentiation(a, b) {
return a ** b;
}
addOpLabel(exponentiation, "«ops.exponentiation»");
/**
* Look up the given key as an external reference and cache the value for future
* requests.
*
* @this {AsyncTree|null}
*/
export async function external(key, cache) {
if (key in cache) {
return cache[key];
}
// First save a promise for the value
const promise = scope.call(this, key);
cache[key] = promise;
const value = await promise;
// Now update with the actual value
cache[key] = value;
return value;
}
/**
* This op is only used during parsing. It signals to ops.object that the
* "arguments" of the expression should be used to define a property getter.
*/
export const getter = new String("«ops.getter»");
export function greaterThan(a, b) {
return a > b;
}
addOpLabel(greaterThan, "«ops.greaterThan»");
export function greaterThanOrEqual(a, b) {
return a >= b;
}
addOpLabel(greaterThanOrEqual, "«ops.greaterThanOrEqual»");
/**
* Files tree for the user's home directory.
*
* @this {AsyncTree|null}
*/
export async function homeDirectory() {
const tree = new OrigamiFiles(os.homedir());
tree.parent = this ? Tree.root(this) : null;
return tree;
}
/**
* Search the parent's scope -- i.e., exclude the current tree -- for the given
* key.
*
* @this {AsyncTree|null}
* @param {*} key
*/
export async function inherited(key) {
if (!this?.parent) {
return undefined;
}
const parentScope = scopeFn(this.parent);
return parentScope.get(key);
}
addOpLabel(inherited, "«ops.inherited»");
/**
* Return a function that will invoke the given code.
*
* @typedef {import("../../index.ts").Code} Code
* @this {AsyncTree|null}
* @param {string[]} parameters
* @param {Code} code
*/
export function lambda(parameters, code) {
const context = this;
/** @this {Treelike|null} */
async function invoke(...args) {
let target;
if (parameters.length === 0) {
// No parameters
target = context;
} else {
// Add arguments to scope.
const ambients = {};
for (const parameter of parameters) {
ambients[parameter] = args.shift();
}
Object.defineProperty(ambients, codeSymbol, {
value: code,
enumerable: false,
});
const ambientTree = new ObjectTree(ambients);
ambientTree.parent = context;
target = ambientTree;
}
let result = await evaluate.call(target, code);
// Bind a function result to the ambients so that it has access to the
// parameter values -- i.e., like a closure.
if (result instanceof Function) {
const resultCode = result.code;
result = result.bind(target);
if (code) {
// Copy over Origami code
result.code = resultCode;
}
}
return result;
}
// We set the `length` property on the function so that Tree.traverseOrThrow()
// will correctly identify how many parameters it wants. This is unorthodox
// but doesn't appear to affect other behavior.
const fnLength = parameters.length;
Object.defineProperty(invoke, "length", {
value: fnLength,
});
invoke.code = code;
return invoke;
}
addOpLabel(lambda, "«ops.lambda");
export function lessThan(a, b) {
return a < b;
}
addOpLabel(lessThan, "«ops.lessThan»");
export function lessThanOrEqual(a, b) {
return a <= b;
}
addOpLabel(lessThanOrEqual, "«ops.lessThanOrEqual»");
/**
* Return a primitive value
*/
export async function literal(value) {
return value;
}
addOpLabel(literal, "«ops.literal»");
/**
* Logical AND operator
*/
export async function logicalAnd(head, ...tail) {
if (!head) {
return head;
}
// Evaluate the tail arguments in order, short-circuiting if any are falsy.
let lastValue;
for (const arg of tail) {
lastValue = arg instanceof Function ? await arg() : arg;
if (!lastValue) {
return lastValue;
}
}
// Return the last value (not `true`)
return lastValue;
}
/**
* Logical NOT operator
*/
export async function logicalNot(value) {
return !value;
}
/**
* Logical OR operator
*/
export async function logicalOr(head, ...tail) {
if (head) {
return head;
}
// Evaluate the tail arguments in order, short-circuiting if any are truthy.
let lastValue;
for (const arg of tail) {
lastValue = arg instanceof Function ? await arg() : arg;
if (lastValue) {
return lastValue;
}
}
return lastValue;
}
/**
* Merge the given trees. If they are all plain objects, return a plain object.
*
* @this {AsyncTree|null}
* @param {import("@weborigami/async-tree").Treelike[]} trees
*/
export async function merge(...trees) {
return mergeTrees.call(this, ...trees);
}
addOpLabel(merge, "«ops.merge»");
export function multiplication(a, b) {
return a * b;
}
addOpLabel(multiplication, "«ops.multiplication»");
export function notEqual(a, b) {
return a != b;
}
addOpLabel(notEqual, "«ops.notEqual»");
export function notStrictEqual(a, b) {
return a !== b;
}
addOpLabel(notStrictEqual, "«ops.notStrictEqual»");
/**
* Nullish coalescing operator
*/
export async function nullishCoalescing(head, ...tail) {
if (head != null) {
return head;
}
let lastValue;
for (const arg of tail) {
lastValue = arg instanceof Function ? await arg() : arg;
if (lastValue != null) {
return lastValue;
}
}
return lastValue;
}
/**
* Construct an object. The keys will be the same as the given `obj`
* parameter's, and the values will be the results of evaluating the
* corresponding code values in `obj`.
*
* @this {AsyncTree|null}
* @param {any[]} entries
*/
export async function object(...entries) {
return expressionObject(entries, this);
}
addOpLabel(object, "«ops.object»");
export function remainder(a, b) {
return a % b;
}
addOpLabel(remainder, "«ops.remainder»");
/**
* Files tree for the filesystem root.
*
* @this {AsyncTree|null}
*/
export async function rootDirectory(key) {
let tree = new OrigamiFiles("/");
// We set the builtins as the parent because logically the filesystem root is
// outside the project. This ignores the edge case where the project itself is
// the root of the filesystem and has a config file.
tree.parent = this ? Tree.root(this) : null;
return key ? tree.get(key) : tree;
}
/**
* Look up the given key in the scope for the current tree.
*
* @this {AsyncTree|null}
*/
export async function scope(key) {
if (!this) {
throw new Error("Tried to get the scope of a null or undefined tree.");
}
const scope = scopeFn(this);
const value = await scope.get(key);
if (value === undefined && key !== "undefined") {
throw await scopeReferenceError(scope, key);
}
return value;
}
addOpLabel(scope, "«ops.scope»");
export function shiftLeft(a, b) {
return a << b;
}
addOpLabel(shiftLeft, "«ops.shiftLeft»");
export function shiftRightSigned(a, b) {
return a >> b;
}
addOpLabel(shiftRightSigned, "«ops.shiftRightSigned»");
export function shiftRightUnsigned(a, b) {
return a >>> b;
}
addOpLabel(shiftRightUnsigned, "«ops.shiftRightUnsigned»");
/**
* The spread operator is a placeholder during parsing. It should be replaced
* with an object merge.
*/
export function spread(...args) {
throw new Error(
"Internal error: a spread operation wasn't compiled correctly."
);
}
addOpLabel(spread, "«ops.spread»");
export function strictEqual(a, b) {
return a === b;
}
addOpLabel(strictEqual, "«ops.strictEqual»");
export function subtraction(a, b) {
return a - b;
}
addOpLabel(subtraction, "«ops.subtraction»");
/**
* Apply the default tagged template function.
*/
export function template(strings, ...values) {
return taggedTemplate(strings, values);
}
addOpLabel(template, "«ops.template»");
/**
* Traverse a path of keys through a tree.
*/
export const traverse = Tree.traverseOrThrow;
export function unaryMinus(a) {
return -a;
}
addOpLabel(unaryMinus, "«ops.unaryMinus»");
export function unaryPlus(a) {
return +a;
}
addOpLabel(unaryPlus, "«ops.unaryPlus»");
/**
* If the value is packed but has an unpack method, call it and return that as
* the result; otherwise, return the value as is.
*
* @param {any} value
*/
export async function unpack(value) {
return isUnpackable(value) ? value.unpack() : value;
}

View File

@@ -0,0 +1,3 @@
export const codeSymbol = Symbol("code");
export const scopeSymbol = Symbol("scope");
export const sourceSymbol = Symbol("source");

View File

@@ -0,0 +1,9 @@
// Default JavaScript tagged template function splices strings and values
// together.
export default function defaultTemplateJoin(strings, values) {
let result = strings[0];
for (let i = 0; i < values.length; i++) {
result += values[i] + strings[i + 1];
}
return result;
}

71
node_modules/@weborigami/language/src/runtime/typos.js generated vendored Normal file
View File

@@ -0,0 +1,71 @@
/**
* Returns true if the two strings have a Damerau-Levenshtein distance of 1.
* This will be true if the strings differ by a single insertion, deletion,
* substitution, or transposition.
*
* @param {string} s1
* @param {string} s2
*/
export function isTypo(s1, s2) {
const length1 = s1.length;
const length2 = s2.length;
// If the strings are identical, distance is 0
if (s1 === s2) {
return false;
}
// If length difference is more than 1, distance can't be 1
if (Math.abs(length1 - length2) > 1) {
return false;
}
if (length1 === length2) {
// Check for one substitution
let differences = 0;
for (let i = 0; i < length1; i++) {
if (s1[i] !== s2[i]) {
differences++;
if (differences > 1) {
break;
}
}
}
if (differences === 1) {
return true;
}
// Check for one transposition
for (let i = 0; i < length1 - 1; i++) {
if (s1[i] !== s2[i]) {
// Check if swapping s1[i] and s1[i+1] matches s2
if (s1[i] === s2[i + 1] && s1[i + 1] === s2[i]) {
return s1.slice(i + 2) === s2.slice(i + 2);
} else {
return false;
}
}
}
}
// Check for one insertion/deletion
const longer = length1 > length2 ? s1 : s2;
const shorter = length1 > length2 ? s2 : s1;
for (let i = 0; i < shorter.length; i++) {
if (shorter[i] !== longer[i]) {
// If we skip this character, do the rest match?
return shorter.slice(i) === longer.slice(i + 1);
}
}
return shorter === longer.slice(0, shorter.length);
}
/**
* Return any strings that could be a typo of s
*
* @param {string} s
* @param {string[]} strings
*/
export function typos(s, strings) {
return strings.filter((str) => isTypo(s, str));
}