1
0

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,18 @@
/**
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
* @implements {AsyncTree}
*/
export default class ConstantTree {
constructor(value) {
this.value = value;
this.parent = null;
}
async get(key) {
return this.value;
}
async keys() {
return [];
}
}

View File

@@ -0,0 +1 @@
export default function assertTreeIsDefined(tree: any, methodName: string): asserts tree is object

View File

@@ -0,0 +1,10 @@
import { Tree } from "@weborigami/async-tree";
export default function assertTreeIsDefined(tree, methodName) {
const isValid = tree === null || Tree.isAsyncTree(tree);
if (!isValid) {
throw new Error(
`${methodName} must be called with a tree target. If you don't want to pass a tree, invoke with: ${methodName}.call(null)`
);
}
}

View File

@@ -0,0 +1,20 @@
import { pathFromKeys } from "@weborigami/async-tree";
/**
* Given a protocol, a host, and a list of keys, construct an href.
*
* @param {string} protocol
* @param {string} host
* @param {string[]} keys
*/
export default function constructHref(protocol, host, ...keys) {
const path = pathFromKeys(keys);
let href = [host, path].join("/");
if (!href.startsWith(protocol)) {
if (!href.startsWith("//")) {
href = `//${href}`;
}
href = `${protocol}${href}`;
}
return href;
}

View File

@@ -0,0 +1,34 @@
import { trailingSlash } from "@weborigami/async-tree";
import { HandleExtensionsTransform } from "@weborigami/language";
import constructHref from "./constructHref.js";
/**
* Given a protocol, a host, and a list of keys, construct an href.
*
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
*
* @param {string} protocol
* @param {import("../../index.ts").Constructor<AsyncTree>} treeClass
* @param {AsyncTree|null} parent
* @param {string} host
* @param {string[]} keys
*/
export default function constructSiteTree(
protocol,
treeClass,
parent,
host,
...keys
) {
// If the last key doesn't end in a slash, remove it for now.
let lastKey;
if (keys.length > 0 && keys.at(-1) && !trailingSlash.has(keys.at(-1))) {
lastKey = keys.pop();
}
const href = constructHref(protocol, host, ...keys);
let result = new (HandleExtensionsTransform(treeClass))(href);
result.parent = parent;
return lastKey ? result.get(lastKey) : result;
}

View File

@@ -0,0 +1,42 @@
import { isPlainObject, isUnpackable, toString } from "@weborigami/async-tree";
// import txtHandler from "../builtins/txt.handler.js";
/**
* In Origami, a text document object is any object with a `@text` property and
* a pack() method that formats that object as text with YAML front matter. This
* function is a helper for constructing such text document objects.
*
* @typedef {import("@weborigami/async-tree").StringLike} StringLike
* @typedef {import("@weborigami/async-tree").PlainObject} PlainObject
*
* @param {StringLike|PlainObject} input
* @param {any} [data]
*/
export default async function documentObject(input, data) {
let text;
let inputData;
if (isUnpackable(input)) {
// Unpack the input first, might already be a document object.
input = await input.unpack();
}
if (isPlainObject(input)) {
text = input["@text"];
inputData = input;
} else {
text = toString(input);
inputData = null;
}
// TODO: Either restore this code, or move responsibility for packing a
// document to HandleExtensionsTransform set().
// const base = {
// pack() {
// return txtHandler.pack(this);
// },
// };
// const result = Object.create(base);
const result = {};
Object.assign(result, inputData, data, { "@text": text });
return result;
}

View File

@@ -0,0 +1,26 @@
import { handleExtension } from "@weborigami/language";
/**
* Fetch the resource at the given href.
*
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
*
* @this {AsyncTree|null}
* @param {string} href
*/
export default async function fetchAndHandleExtension(href) {
const response = await fetch(href);
if (!response.ok) {
return undefined;
}
let buffer = await response.arrayBuffer();
// Attach any loader defined for the file type.
const url = new URL(href);
const filename = url.pathname.split("/").pop();
if (this && filename) {
buffer = await handleExtension(this, buffer, filename);
}
return buffer;
}

View File

@@ -0,0 +1,67 @@
import { Tree, isUnpackable } from "@weborigami/async-tree";
import assertTreeIsDefined from "./assertTreeIsDefined.js";
/**
* Many Origami built-in functions accept an optional treelike object as their
* first argument. If no tree is supplied, then the current context for the
* Origami command is used as the tree.
*
* So the argument is optional -- but if supplied, it must be defined. The
* caller should pass its `arguments` object to this function so that the actual
* number of supplied arguments can be checked.
*
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
* @typedef {import("@weborigami/async-tree").Treelike} Treelike
*
* @param {AsyncTree|null} parent
* @param {IArguments} args
* @param {Treelike|undefined} treelike
* @param {string} methodName
* @param {boolean} [deep]
* @returns {Promise<AsyncTree>}
*/
export default async function getTreeArgument(
parent,
args,
treelike,
methodName,
deep
) {
assertTreeIsDefined(parent, methodName);
if (treelike !== undefined) {
if (isUnpackable(treelike)) {
treelike = await treelike.unpack();
}
if (Tree.isTreelike(treelike)) {
const options = deep !== undefined ? { deep } : undefined;
let tree = Tree.from(treelike, options);
// If the tree was created from a treelike object and does not yet have a
// parent, make the current tree its parent.
if (!tree.parent && parent !== undefined) {
if (parent !== null && !Tree.isAsyncTree(parent)) {
throw new Error(
`The parent argument passed to ${methodName} must be a tree.`
);
}
tree.parent = parent;
}
return tree;
}
throw new Error(
`The first argument to ${methodName} must be a tree, like an array, object, or files.`
);
}
if (args.length === 0) {
if (!parent) {
// Should never happen because assertTreeIsDefined throws an exception.
throw new Error(
`${methodName} was called with no tree argument and no parent.`
);
}
return parent;
}
throw new Error(`The first argument to ${methodName} was undefined.`);
}

View File

@@ -0,0 +1,38 @@
import { symbols, Tree } from "@weborigami/async-tree";
import { builtinsTree } from "../internal.js";
/**
* Perform any necessary post-processing on the unpacked content of a file. This
* lets treat the contents of various file types consistently.
*
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
*
* @param {any} content
* @param {AsyncTree|null} parent
* @returns
*/
export default function processUnpackedContent(content, parent) {
if (typeof content === "function") {
// Bind the function to the parent as the `this` context.
const target = parent ?? builtinsTree;
const result = content.bind(target);
if (content.code) {
result.code = content.code;
}
return result;
} else if (Tree.isAsyncTree(content) && !content.parent) {
const result = Object.create(content);
result.parent = parent;
return result;
} else if (Object.isExtensible(content) && !content[symbols.parent]) {
Object.defineProperty(content, symbols.parent, {
configurable: true,
enumerable: false,
value: parent,
writable: true,
});
return content;
} else {
return content;
}
}

View File

@@ -0,0 +1,7 @@
import type { AsyncTree } from "@weborigami/types";
import type { JsonValue } from "../../index.ts";
export function evaluateYaml(text: string, parent?: AsyncTree|null): Promise<JsonValue>;
export function parseYaml(text: string): JsonValue|AsyncTree;
export function toJson(obj: JsonValue | AsyncTree): Promise<string>;
export function toYaml(obj: JsonValue | AsyncTree): Promise<string>;

View File

@@ -0,0 +1,58 @@
/**
* @typedef {import("../../index.ts").JsonValue} JsonValue
* @typedef {import("@weborigami/async-tree").PlainObject} PlainObject
* @typedef {import("@weborigami/async-tree").Treelike} Treelike
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
*/
import { Tree, toPlainValue } from "@weborigami/async-tree";
import * as YAMLModule from "yaml";
// The "yaml" package doesn't seem to provide a default export that the browser can
// recognize, so we have to handle two ways to accommodate Node and the browser.
// @ts-ignore
const YAML = YAMLModule.default ?? YAMLModule.YAML;
/**
*
* @param {string} text
* @param {AsyncTree|null} [parent]
*/
export async function evaluateYaml(text, parent) {
const data = parseYaml(String(text));
if (Tree.isAsyncTree(data)) {
data.parent = parent;
return Tree.plain(data);
} else {
return data;
}
}
/**
* @param {string} text
* @returns {JsonValue|AsyncTree}
*/
export function parseYaml(text) {
return YAML.parse(text);
}
/**
* Serializes an object as a JSON string.
*
* @param {any} obj
*/
export async function toJson(obj) {
const serializable = await toPlainValue(obj);
return JSON.stringify(serializable, null, 2);
}
/**
* Serializes an object as a JSON string.
*
* @param {any} obj
* @returns {Promise<string>}
*/
export async function toYaml(obj) {
const serializable = await toPlainValue(obj);
return YAML.stringify(serializable);
}

View File

@@ -0,0 +1,7 @@
export function getDescriptor(object: any): string;
export function hasNonPrintableCharacters(text: string): boolean;
export function isTransformApplied(Transform: Function, object: any): boolean;
export function toFunction(object: any): Function;
export function toString(object: any): string|null;
export function transformObject(Transform: Function, object: any): any;

View File

@@ -0,0 +1,144 @@
import {
Tree,
toString as asyncTreeToString,
isPlainObject,
isUnpackable,
trailingSlash,
} from "@weborigami/async-tree";
import { symbols } from "@weborigami/language";
import { basename } from "node:path";
// For a given tree, return some user-friendly descriptor
export function getDescriptor(tree) {
if ("path" in tree) {
return trailingSlash.add(basename(tree.path));
}
const source = tree[symbols.sourceSymbol];
if (source) {
// If the source looks like an identifier, use that.
// TODO: Use real identifier parsing.
const identifierRegex = /^[A-Za-z0-9_\-\.]+\/?$/;
if (identifierRegex.test(source)) {
return trailingSlash.add(source);
}
}
return null;
}
// Return true if the text appears to contain non-printable binary characters;
// used to infer whether a file is binary or text.
export function hasNonPrintableCharacters(text) {
// https://stackoverflow.com/a/1677660/76472
return /[\x00-\x08\x0E-\x1F]/.test(text);
}
export function isTransformApplied(Transform, obj) {
let transformName = Transform.name;
if (!transformName) {
throw `isTransformApplied was called on an unnamed transform function, but a name is required.`;
}
if (transformName.endsWith("Transform")) {
transformName = transformName.slice(0, -9);
}
// Walk up prototype chain looking for a constructor with the same name as the
// transform. This is not a great test.
for (let proto = obj; proto; proto = Object.getPrototypeOf(proto)) {
if (proto.constructor.name === transformName) {
return true;
}
}
return false;
}
/**
* Convert the given object to a function.
*
* @typedef {import("../../index.ts").Invocable} Invocable
* @param {any} obj
* @returns {Function|null}
*/
export function toFunction(obj) {
if (typeof obj === "function") {
// Return a function as is.
return obj;
} else if (isUnpackable(obj)) {
// Extract the contents of the object and convert that to a function.
let fnPromise;
/** @this {any} */
return async function (...args) {
if (!fnPromise) {
// unpack() may return a function or a promise for a function; normalize
// to a promise for a function
const unpackPromise = Promise.resolve(obj.unpack());
fnPromise = unpackPromise.then((content) => toFunction(content));
}
const fn = await fnPromise;
return fn.call(this, ...args);
};
} else if (Tree.isTreelike(obj)) {
// Return a function that invokes the tree's getter.
return Tree.toFunction(obj);
} else {
// Not a function
return null;
}
}
/**
* Extend the async-tree toString method: objects that have a `@text` property
* will return the value of that property as a string.
*
* @param {any} object
* @returns {string|null}
*/
export function toString(object) {
if (isPlainObject(object) && "@text" in object) {
object = object["@text"];
}
return asyncTreeToString(object);
}
/**
* Apply a functional class mixin to an individual object instance.
*
* This works by create an intermediate class, creating an instance of that, and
* then setting the intermediate class's prototype to the given individual
* object. The resulting, extended object is then returned.
*
* This manipulation of the prototype chain is generally sound in JavaScript,
* with some caveats. In particular, the original object class cannot make
* direct use of private members; JavaScript will complain if the extended
* object does anything that requires access to those private members.
*
* @param {Function} Transform
* @param {any} obj
*/
export function transformObject(Transform, obj) {
// Apply the mixin to Object and instantiate that. The Object base class here
// is going to be cut out of the prototype chain in a moment; we just use
// Object as a convenience because its constructor takes no arguments.
const mixed = new (Transform(Object))();
// Find the highest prototype in the chain that was added by the class mixin.
// The mixin may have added multiple prototypes to the chain. Walk up the
// prototype chain until we hit Object.
let mixinProto = Object.getPrototypeOf(mixed);
while (Object.getPrototypeOf(mixinProto) !== Object.prototype) {
mixinProto = Object.getPrototypeOf(mixinProto);
}
// Redirect the prototype chain above the mixin to point to the original
// object. The mixed object now extends the original object with the mixin.
Object.setPrototypeOf(mixinProto, obj);
// Create a new constructor for this mixed object that reflects its prototype
// chain. Because we've already got the instance we want, we won't use this
// constructor now, but this can be used later to instantiate other objects
// that look like the mixed one.
mixed.constructor = Transform(obj.constructor);
// Return the mixed object.
return mixed;
}