forked from sashin/sashinexists
run npm install to generate a package lock
This commit is contained in:
63
node_modules/@weborigami/language/index.ts
generated
vendored
Normal file
63
node_modules/@weborigami/language/index.ts
generated
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Packed } from "@weborigami/async-tree";
|
||||
|
||||
export * from "./main.js";
|
||||
|
||||
/**
|
||||
* A chunk of compiled Origami code. This is just an array with an additional
|
||||
* `location` property.
|
||||
*/
|
||||
export type Code = Array<any> & {
|
||||
location: {
|
||||
source: Source;
|
||||
start: Position;
|
||||
end: Position;
|
||||
};
|
||||
source: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A class constructor is an object with a `new` method that returns an
|
||||
* instance of the indicated type.
|
||||
*/
|
||||
export type Constructor<T> = new (...args: any[]) => T;
|
||||
|
||||
/**
|
||||
* A structure associating a media type and an unpack function with a given file
|
||||
* extension.
|
||||
*/
|
||||
export type ExtensionHandler = {
|
||||
mediaType?: string;
|
||||
unpack?: UnpackFunction;
|
||||
}
|
||||
|
||||
export type Position = {
|
||||
column: number;
|
||||
line: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A mixin is a function that takes an existing class and returns a new class.
|
||||
*
|
||||
* The use of a generic type `T` here is a way of indicating that the members of
|
||||
* the supplied base class automatically pass through to the result. That
|
||||
* ensures the use of the mixin doesn't accidentally hide members of the class
|
||||
* passed to the mixin.
|
||||
*/
|
||||
export type Mixin<MixinMembers> = <T>(
|
||||
Base: Constructor<T>
|
||||
) => Constructor<T & MixinMembers>;
|
||||
|
||||
/**
|
||||
* Source code representation used by the parser.
|
||||
*/
|
||||
export type Source = {
|
||||
name?: string;
|
||||
text: string;
|
||||
url?: URL;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that converts a value from a persistent form into a live value.
|
||||
*/
|
||||
export type UnpackFunction = (input: Packed, options?: any) => any;
|
||||
17
node_modules/@weborigami/language/main.js
generated
vendored
Normal file
17
node_modules/@weborigami/language/main.js
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
export * from "./src/runtime/internal.js";
|
||||
|
||||
export * as compile from "./src/compiler/compile.js";
|
||||
export * from "./src/runtime/errors.js";
|
||||
export { default as evaluate } from "./src/runtime/evaluate.js";
|
||||
export { default as EventTargetMixin } from "./src/runtime/EventTargetMixin.js";
|
||||
export * as expressionFunction from "./src/runtime/expressionFunction.js";
|
||||
export { default as functionResultsMap } from "./src/runtime/functionResultsMap.js";
|
||||
export { default as HandleExtensionsTransform } from "./src/runtime/HandleExtensionsTransform.js";
|
||||
export * from "./src/runtime/handlers.js";
|
||||
export { default as ImportModulesMixin } from "./src/runtime/ImportModulesMixin.js";
|
||||
export { default as InvokeFunctionsTransform } from "./src/runtime/InvokeFunctionsTransform.js";
|
||||
export { default as OrigamiFiles } from "./src/runtime/OrigamiFiles.js";
|
||||
export * as symbols from "./src/runtime/symbols.js";
|
||||
export { default as taggedTemplate } from "./src/runtime/taggedTemplate.js";
|
||||
export { default as TreeEvent } from "./src/runtime/TreeEvent.js";
|
||||
export { default as WatchFilesMixin } from "./src/runtime/WatchFilesMixin.js";
|
||||
26
node_modules/@weborigami/language/package.json
generated
vendored
Normal file
26
node_modules/@weborigami/language/package.json
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@weborigami/language",
|
||||
"version": "0.2.1",
|
||||
"description": "Web Origami expression language compiler and runtime",
|
||||
"type": "module",
|
||||
"main": "./main.js",
|
||||
"types": "./index.ts",
|
||||
"devDependencies": {
|
||||
"@types/node": "22.7.4",
|
||||
"peggy": "4.0.3",
|
||||
"typescript": "5.6.2",
|
||||
"yaml": "2.5.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@weborigami/async-tree": "0.2.1",
|
||||
"@weborigami/types": "0.2.1",
|
||||
"watcher": "2.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "peggy --allowed-start-rules=\"*\" --format es src/compiler/origami.pegjs --output src/compiler/parse.js",
|
||||
"buildTests": "node ./test/generator/generateTests.js",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "node --test --test-reporter=spec",
|
||||
"typecheck": "tsc"
|
||||
}
|
||||
}
|
||||
119
node_modules/@weborigami/language/src/compiler/compile.js
generated
vendored
Normal file
119
node_modules/@weborigami/language/src/compiler/compile.js
generated
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
import { trailingSlash } from "@weborigami/async-tree";
|
||||
import { createExpressionFunction } from "../runtime/expressionFunction.js";
|
||||
import { ops } from "../runtime/internal.js";
|
||||
import { parse } from "./parse.js";
|
||||
import { annotate, undetermined } from "./parserHelpers.js";
|
||||
|
||||
function compile(source, options) {
|
||||
const { startRule } = options;
|
||||
const enableCaching = options.scopeCaching ?? true;
|
||||
if (typeof source === "string") {
|
||||
source = { text: source };
|
||||
}
|
||||
const code = parse(source.text, {
|
||||
grammarSource: source,
|
||||
startRule,
|
||||
});
|
||||
const cache = {};
|
||||
const modified = transformScopeReferences(code, cache, enableCaching);
|
||||
const fn = createExpressionFunction(modified);
|
||||
return fn;
|
||||
}
|
||||
|
||||
export function expression(source, options = {}) {
|
||||
return compile(source, {
|
||||
...options,
|
||||
startRule: "expression",
|
||||
});
|
||||
}
|
||||
|
||||
// Transform any remaining undetermined references to scope references. At the
|
||||
// same time, transform those or explicit ops.scope calls to ops.external calls
|
||||
// unless they refer to local variables (variables defined by object literals or
|
||||
// lambda parameters).
|
||||
export function transformScopeReferences(
|
||||
code,
|
||||
cache,
|
||||
enableCaching,
|
||||
locals = {}
|
||||
) {
|
||||
const [fn, ...args] = code;
|
||||
|
||||
let additionalLocalNames;
|
||||
switch (fn) {
|
||||
case undetermined:
|
||||
case ops.scope:
|
||||
const key = args[0];
|
||||
const normalizedKey = trailingSlash.remove(key);
|
||||
if (enableCaching && !locals[normalizedKey]) {
|
||||
// Upgrade to cached external reference
|
||||
const modified = [ops.external, key, cache];
|
||||
// @ts-ignore
|
||||
annotate(modified, code.location);
|
||||
return modified;
|
||||
} else if (fn === undetermined) {
|
||||
// Transform undetermined reference to regular scope call
|
||||
const modified = [ops.scope, key];
|
||||
// @ts-ignore
|
||||
annotate(modified, code.location);
|
||||
return modified;
|
||||
} else {
|
||||
// Internal ops.scope call; leave as is
|
||||
return code;
|
||||
}
|
||||
|
||||
case ops.lambda:
|
||||
const parameters = args[0];
|
||||
additionalLocalNames = parameters;
|
||||
break;
|
||||
|
||||
case ops.object:
|
||||
const entries = args;
|
||||
additionalLocalNames = entries.map(([key]) => trailingSlash.remove(key));
|
||||
break;
|
||||
}
|
||||
|
||||
let updatedLocals = { ...locals };
|
||||
if (additionalLocalNames) {
|
||||
for (const key of additionalLocalNames) {
|
||||
updatedLocals[key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
const modified = code.map((child) => {
|
||||
if (Array.isArray(child)) {
|
||||
// Review: This currently descends into arrays that are not instructions,
|
||||
// such as the parameters of a lambda. This should be harmless, but it'd
|
||||
// be preferable to only descend into instructions. This would require
|
||||
// surrounding ops.lambda parameters with ops.literal, and ops.object
|
||||
// entries with ops.array.
|
||||
return transformScopeReferences(
|
||||
child,
|
||||
cache,
|
||||
enableCaching,
|
||||
updatedLocals
|
||||
);
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
});
|
||||
|
||||
if (code.location) {
|
||||
annotate(modified, code.location);
|
||||
}
|
||||
return modified;
|
||||
}
|
||||
|
||||
export function program(source, options = {}) {
|
||||
return compile(source, {
|
||||
...options,
|
||||
startRule: "program",
|
||||
});
|
||||
}
|
||||
|
||||
export function templateDocument(source, options = {}) {
|
||||
return compile(source, {
|
||||
...options,
|
||||
startRule: "templateDocument",
|
||||
});
|
||||
}
|
||||
635
node_modules/@weborigami/language/src/compiler/origami.pegjs
generated
vendored
Normal file
635
node_modules/@weborigami/language/src/compiler/origami.pegjs
generated
vendored
Normal file
@@ -0,0 +1,635 @@
|
||||
{{
|
||||
//
|
||||
// Origami language parser
|
||||
//
|
||||
// This generally follows the pattern of the JavaScript expression grammar at
|
||||
// https://github.com/pegjs/pegjs/blob/master/examples/javascript.pegjs. Like
|
||||
// that parser, this one uses the ECMAScript grammar terms where relevant.
|
||||
//
|
||||
// Generate the parser via `npm build`.
|
||||
//
|
||||
// @ts-nocheck
|
||||
//
|
||||
|
||||
import * as ops from "../runtime/ops.js";
|
||||
import {
|
||||
annotate,
|
||||
downgradeReference,
|
||||
makeArray,
|
||||
makeBinaryOperation,
|
||||
makeCall,
|
||||
makeDeferredArguments,
|
||||
makeObject,
|
||||
makePipeline,
|
||||
makeProperty,
|
||||
makeReference,
|
||||
makeTemplate,
|
||||
makeUnaryOperation
|
||||
} from "./parserHelpers.js";
|
||||
|
||||
}}
|
||||
|
||||
// A block of optional whitespace
|
||||
__
|
||||
= (inlineSpace / newLine / comment)* {
|
||||
return null;
|
||||
}
|
||||
|
||||
additiveExpression
|
||||
= head:multiplicativeExpression tail:(__ @additiveOperator __ @multiplicativeExpression)* {
|
||||
return annotate(tail.reduce(makeBinaryOperation, head), location());
|
||||
}
|
||||
|
||||
additiveOperator
|
||||
= "+"
|
||||
/ "-"
|
||||
|
||||
arguments "function arguments"
|
||||
= parenthesesArguments
|
||||
/ pathArguments
|
||||
/ templateLiteral
|
||||
|
||||
arrayLiteral "array"
|
||||
= "[" __ entries:arrayEntries? __ closingBracket {
|
||||
return annotate(makeArray(entries ?? []), location());
|
||||
}
|
||||
|
||||
// A separated list of array entries
|
||||
arrayEntries
|
||||
= entries:arrayEntry|1.., separator| separator? {
|
||||
return annotate(entries, location());
|
||||
}
|
||||
|
||||
arrayEntry
|
||||
= spread
|
||||
/ pipelineExpression
|
||||
// JavaScript treats a missing value as `undefined`
|
||||
/ __ !"]" {
|
||||
return annotate([ops.literal, undefined], location());
|
||||
}
|
||||
|
||||
arrowFunction
|
||||
= "(" __ parameters:identifierList? __ ")" __ doubleArrow __ pipeline:pipelineExpression {
|
||||
return annotate([ops.lambda, parameters ?? [], pipeline], location());
|
||||
}
|
||||
/ identifier:identifier __ doubleArrow __ pipeline:pipelineExpression {
|
||||
return annotate([ops.lambda, [identifier], pipeline], location());
|
||||
}
|
||||
/ conditionalExpression
|
||||
|
||||
bitwiseAndExpression
|
||||
= head:equalityExpression tail:(__ @bitwiseAndOperator __ @equalityExpression)* {
|
||||
return annotate(tail.reduce(makeBinaryOperation, head), location());
|
||||
}
|
||||
|
||||
bitwiseAndOperator
|
||||
= @"&" !"&"
|
||||
|
||||
bitwiseOrExpression
|
||||
= head:bitwiseXorExpression tail:(__ @bitwiseOrOperator __ @bitwiseXorExpression)* {
|
||||
return annotate(tail.reduce(makeBinaryOperation, head), location());
|
||||
}
|
||||
|
||||
bitwiseOrOperator
|
||||
= @"|" !"|"
|
||||
|
||||
bitwiseXorExpression
|
||||
= head:bitwiseAndExpression tail:(__ @bitwiseXorOperator __ @bitwiseAndExpression)* {
|
||||
return annotate(tail.reduce(makeBinaryOperation, head), location());
|
||||
}
|
||||
|
||||
bitwiseXorOperator
|
||||
= "^"
|
||||
|
||||
// A function call: `fn(arg)`, possibly part of a chain of function calls, like
|
||||
// `fn(arg1)(arg2)(arg3)`.
|
||||
callExpression "function call"
|
||||
= head:protocolExpression tail:arguments* {
|
||||
return annotate(tail.reduce(makeCall, head), location());
|
||||
}
|
||||
|
||||
// Required closing curly brace. We use this for the `object` term: if the
|
||||
// parser sees a left curly brace, here we must see a right curly brace.
|
||||
closingBrace
|
||||
= "}"
|
||||
/ .? {
|
||||
error("Expected right curly brace");
|
||||
}
|
||||
|
||||
// Required closing bracket
|
||||
closingBracket
|
||||
= "]"
|
||||
/ .? {
|
||||
error("Expected right bracket");
|
||||
}
|
||||
|
||||
// Required closing parenthesis. We use this for the `group` term: it's the last
|
||||
// term in the `step` parser that starts with a parenthesis, so if that parser
|
||||
// sees a left parenthesis, here we must see a right parenthesis.
|
||||
closingParenthesis
|
||||
= ")"
|
||||
/ .? {
|
||||
error("Expected right parenthesis");
|
||||
}
|
||||
|
||||
// A comma-separated list of expressions: `x, y, z`
|
||||
commaExpression
|
||||
// The commas are required, but the list can have a single item.
|
||||
= list:pipelineExpression|1.., __ "," __ | {
|
||||
return list.length === 1
|
||||
? list[0]
|
||||
: annotate([ops.comma, ...list], location());
|
||||
}
|
||||
|
||||
// A single line comment
|
||||
comment "comment"
|
||||
= multiLineComment
|
||||
/ singleLineComment
|
||||
|
||||
conditionalExpression
|
||||
= condition:logicalOrExpression __
|
||||
"?" __ truthy:pipelineExpression __
|
||||
":" __ falsy:pipelineExpression
|
||||
{
|
||||
return annotate([
|
||||
ops.conditional,
|
||||
downgradeReference(condition),
|
||||
[ops.lambda, [], downgradeReference(truthy)],
|
||||
[ops.lambda, [], downgradeReference(falsy)]
|
||||
], location());
|
||||
}
|
||||
/ logicalOrExpression
|
||||
|
||||
digits
|
||||
= @[0-9]+
|
||||
|
||||
doubleArrow = "⇒" / "=>"
|
||||
|
||||
doubleQuoteString "double quote string"
|
||||
= '"' chars:doubleQuoteStringChar* '"' {
|
||||
return annotate([ops.literal, chars.join("")], location());
|
||||
}
|
||||
|
||||
doubleQuoteStringChar
|
||||
= !('"' / newLine) @textChar
|
||||
|
||||
ellipsis = "..." / "…" // Unicode ellipsis
|
||||
|
||||
equalityExpression
|
||||
= head:relationalExpression tail:(__ @equalityOperator __ @relationalExpression)* {
|
||||
return annotate(tail.reduce(makeBinaryOperation, head), location());
|
||||
}
|
||||
|
||||
equalityOperator
|
||||
= "==="
|
||||
/ "!=="
|
||||
/ "=="
|
||||
/ "!="
|
||||
|
||||
escapedChar "backslash-escaped character"
|
||||
= "\\0" { return "\0"; }
|
||||
/ "\\b" { return "\b"; }
|
||||
/ "\\f" { return "\f"; }
|
||||
/ "\\n" { return "\n"; }
|
||||
/ "\\r" { return "\r"; }
|
||||
/ "\\t" { return "\t"; }
|
||||
/ "\\v" { return "\v"; }
|
||||
/ "\\" @.
|
||||
|
||||
exponentiationExpression
|
||||
= left:unaryExpression __ "**" __ right:exponentiationExpression {
|
||||
return annotate([ops.exponentiation, left, right], location());
|
||||
}
|
||||
/ unaryExpression
|
||||
|
||||
// A top-level expression, possibly with leading/trailing whitespace
|
||||
expression
|
||||
= __ @commaExpression __
|
||||
|
||||
floatLiteral "floating-point number"
|
||||
= digits? "." digits {
|
||||
return annotate([ops.literal, parseFloat(text())], location());
|
||||
}
|
||||
|
||||
// An expression in parentheses: `(foo)`
|
||||
group "parenthetical group"
|
||||
= "(" expression:expression closingParenthesis {
|
||||
return annotate(downgradeReference(expression), location());
|
||||
}
|
||||
|
||||
guillemetString "guillemet string"
|
||||
= '«' chars:guillemetStringChar* '»' {
|
||||
return annotate([ops.literal, chars.join("")], location());
|
||||
}
|
||||
|
||||
guillemetStringChar
|
||||
= !('»' / newLine) @textChar
|
||||
|
||||
// The user's home directory: `~`
|
||||
homeDirectory
|
||||
= "~" {
|
||||
return annotate([ops.homeDirectory], location());
|
||||
}
|
||||
|
||||
// A host identifier that may include a colon and port number: `example.com:80`.
|
||||
// This is used as a special case at the head of a path, where we want to
|
||||
// interpret a colon as part of a text identifier.
|
||||
host "HTTP/HTTPS host"
|
||||
= identifier:identifier port:(":" @integerLiteral)? slashFollows:slashFollows? {
|
||||
const portText = port ? `:${port[1]}` : "";
|
||||
const slashText = slashFollows ? "/" : "";
|
||||
const hostText = identifier + portText + slashText;
|
||||
return annotate([ops.literal, hostText], location());
|
||||
}
|
||||
|
||||
identifier "identifier"
|
||||
= chars:identifierChar+ { return chars.join(""); }
|
||||
|
||||
identifierChar
|
||||
= [^(){}\[\]<>\?!\|\-=,/:\`"'«»\\→⇒… \t\n\r] // No unescaped whitespace or special chars
|
||||
/ @'-' !'>' // Accept a hyphen but not in a single arrow combination
|
||||
/ escapedChar
|
||||
|
||||
identifierList
|
||||
= list:identifier|1.., separator| separator? {
|
||||
return annotate(list, location());
|
||||
}
|
||||
|
||||
implicitParenthesesCallExpression "function call with implicit parentheses"
|
||||
= head:arrowFunction args:(inlineSpace+ @implicitParensthesesArguments)? {
|
||||
return args ? makeCall(head, args) : head;
|
||||
}
|
||||
|
||||
// A separated list of values for an implicit parens call. This differs from
|
||||
// `list` in that the value term can't be a pipeline.
|
||||
implicitParensthesesArguments
|
||||
= values:shorthandFunction|1.., separator| separator? {
|
||||
return annotate(values, location());
|
||||
}
|
||||
|
||||
inlineSpace
|
||||
= [ \t]
|
||||
|
||||
integerLiteral "integer"
|
||||
= digits {
|
||||
return annotate([ops.literal, parseInt(text())], location());
|
||||
}
|
||||
|
||||
// A separated list of values
|
||||
list "list"
|
||||
= values:pipelineExpression|1.., separator| separator? {
|
||||
return annotate(values, location());
|
||||
}
|
||||
|
||||
literal
|
||||
= numericLiteral
|
||||
/ stringLiteral
|
||||
|
||||
logicalAndExpression
|
||||
= head:bitwiseOrExpression tail:(__ "&&" __ @bitwiseOrExpression)* {
|
||||
return tail.length === 0
|
||||
? head
|
||||
: annotate(
|
||||
[ops.logicalAnd, downgradeReference(head), ...makeDeferredArguments(tail)],
|
||||
location()
|
||||
);
|
||||
}
|
||||
|
||||
logicalOrExpression
|
||||
= head:nullishCoalescingExpression tail:(__ "||" __ @nullishCoalescingExpression)* {
|
||||
return tail.length === 0
|
||||
? head
|
||||
: annotate(
|
||||
[ops.logicalOr, downgradeReference(head), ...makeDeferredArguments(tail)],
|
||||
location()
|
||||
);
|
||||
}
|
||||
|
||||
multiLineComment
|
||||
= "/*" (!"*/" .)* "*/" { return null; }
|
||||
|
||||
multiplicativeExpression
|
||||
= head:exponentiationExpression tail:(__ @multiplicativeOperator __ @exponentiationExpression)* {
|
||||
return annotate(tail.reduce(makeBinaryOperation, head), location());
|
||||
}
|
||||
|
||||
multiplicativeOperator
|
||||
= "*"
|
||||
/ "/"
|
||||
/ "%"
|
||||
|
||||
// A namespace reference is a string of letters only, followed by a colon.
|
||||
// For the time being, we also allow a leading `@`, which is deprecated.
|
||||
namespace
|
||||
= at:"@"? chars:[A-Za-z]+ ":" {
|
||||
return annotate([ops.builtin, (at ?? "") + chars.join("") + ":"], location());
|
||||
}
|
||||
|
||||
newLine
|
||||
= "\n"
|
||||
/ "\r\n"
|
||||
/ "\r"
|
||||
|
||||
// A number
|
||||
numericLiteral "number"
|
||||
= floatLiteral
|
||||
/ integerLiteral
|
||||
|
||||
nullishCoalescingExpression
|
||||
= head:logicalAndExpression tail:(__ "??" __ @logicalAndExpression)* {
|
||||
return tail.length === 0
|
||||
? head
|
||||
: annotate(
|
||||
[ops.nullishCoalescing, downgradeReference(head), ...makeDeferredArguments(tail)],
|
||||
location()
|
||||
);
|
||||
}
|
||||
|
||||
// An object literal: `{foo: 1, bar: 2}`
|
||||
objectLiteral "object literal"
|
||||
= "{" __ entries:objectEntries? __ closingBrace {
|
||||
return annotate(makeObject(entries ?? [], ops.object), location());
|
||||
}
|
||||
|
||||
// A separated list of object entries
|
||||
objectEntries
|
||||
= entries:objectEntry|1.., separator| separator? {
|
||||
return annotate(entries, location());
|
||||
}
|
||||
|
||||
objectEntry
|
||||
= spread
|
||||
/ objectProperty
|
||||
/ objectGetter
|
||||
/ objectShorthandProperty
|
||||
|
||||
// A getter definition inside an object literal: `foo = 1`
|
||||
objectGetter "object getter"
|
||||
= key:objectKey __ "=" __ pipeline:pipelineExpression {
|
||||
return annotate(
|
||||
makeProperty(key, annotate([ops.getter, pipeline], location())),
|
||||
location()
|
||||
);
|
||||
}
|
||||
|
||||
objectHiddenKey
|
||||
= hiddenKey:("(" objectPublicKey ")") { return hiddenKey.join(""); }
|
||||
|
||||
objectKey "object key"
|
||||
= objectHiddenKey
|
||||
/ objectPublicKey
|
||||
|
||||
// A property definition in an object literal: `x: 1`
|
||||
objectProperty "object property"
|
||||
= key:objectKey __ ":" __ pipeline:pipelineExpression {
|
||||
return annotate(makeProperty(key, pipeline), location());
|
||||
}
|
||||
|
||||
// A shorthand reference inside an object literal: `foo`
|
||||
objectShorthandProperty "object identifier"
|
||||
= key:objectPublicKey {
|
||||
return annotate([key, [ops.inherited, key]], location());
|
||||
}
|
||||
|
||||
objectPublicKey
|
||||
= identifier:identifier slash:"/"? {
|
||||
return identifier + (slash ?? "");
|
||||
}
|
||||
/ string:stringLiteral {
|
||||
// Remove `ops.literal` from the string code
|
||||
return string[1];
|
||||
}
|
||||
|
||||
// Function arguments in parentheses
|
||||
parenthesesArguments "function arguments in parentheses"
|
||||
= "(" __ list:list? __ ")" {
|
||||
return annotate(list ?? [undefined], location());
|
||||
}
|
||||
|
||||
// A slash-separated path of keys: `a/b/c`
|
||||
path "slash-separated path"
|
||||
// Path with at least a tail
|
||||
= segments:pathSegment|1..| {
|
||||
// Drop empty segments that represent consecutive or final slashes
|
||||
segments = segments.filter(segment => segment);
|
||||
return annotate(segments, location());
|
||||
}
|
||||
|
||||
// A slash-separated path of keys that follows a call target
|
||||
pathArguments
|
||||
= path:path {
|
||||
return annotate([ops.traverse, ...path], location());
|
||||
}
|
||||
|
||||
// A single key in a slash-separated path: `/a`
|
||||
pathKey
|
||||
= chars:pathSegmentChar+ slashFollows:slashFollows? {
|
||||
// Append a trailing slash if one follows (but don't consume it)
|
||||
const key = chars.join("") + (slashFollows ? "/" : "");
|
||||
return annotate([ops.literal, key], location());
|
||||
}
|
||||
|
||||
pathSegment
|
||||
= "/" @pathKey?
|
||||
|
||||
// A single character in a slash-separated path segment
|
||||
pathSegmentChar
|
||||
// This is more permissive than an identifier. It allows some characters like
|
||||
// brackets or quotes that are not allowed in identifiers.
|
||||
= [^(){}\[\],:/\\ \t\n\r]
|
||||
/ escapedChar
|
||||
|
||||
// A pipeline that starts with a value and optionally applies a series of
|
||||
// functions to it.
|
||||
pipelineExpression
|
||||
= head:shorthandFunction tail:(__ singleArrow __ @shorthandFunction)* {
|
||||
return annotate(
|
||||
tail.reduce(makePipeline, downgradeReference(head)),
|
||||
location()
|
||||
);
|
||||
}
|
||||
|
||||
primary
|
||||
= literal
|
||||
/ arrayLiteral
|
||||
/ objectLiteral
|
||||
/ group
|
||||
/ templateLiteral
|
||||
/ reference
|
||||
|
||||
// Top-level Origami progam with possible shebang directive (which is ignored)
|
||||
program "Origami program"
|
||||
= shebang? @expression
|
||||
|
||||
// Protocol with double-slash path: `https://example.com/index.html`
|
||||
protocolExpression
|
||||
= fn:namespace "//" host:host path:path? {
|
||||
const keys = annotate([host, ...(path ?? [])], location());
|
||||
return annotate(makeCall(fn, keys), location());
|
||||
}
|
||||
/ primary
|
||||
|
||||
// A namespace followed by a key: `foo:x`
|
||||
qualifiedReference
|
||||
= fn:namespace reference:scopeReference {
|
||||
const literal = annotate([ops.literal, reference[1]], reference.location);
|
||||
return annotate(makeCall(fn, [literal]), location());
|
||||
}
|
||||
|
||||
reference
|
||||
= rootDirectory
|
||||
/ homeDirectory
|
||||
/ qualifiedReference
|
||||
/ namespace
|
||||
/ scopeReference
|
||||
|
||||
relationalExpression
|
||||
= head:shiftExpression tail:(__ @relationalOperator __ @shiftExpression)* {
|
||||
return annotate(tail.reduce(makeBinaryOperation, head), location());
|
||||
}
|
||||
|
||||
relationalOperator
|
||||
= "<="
|
||||
/ "<"
|
||||
/ ">="
|
||||
/ ">"
|
||||
|
||||
// A top-level folder below the root: `/foo`
|
||||
// or the root folder itself: `/`
|
||||
rootDirectory
|
||||
= "/" key:pathKey {
|
||||
return annotate([ops.rootDirectory, key], location());
|
||||
}
|
||||
/ "/" !"/" {
|
||||
return annotate([ops.rootDirectory], location());
|
||||
}
|
||||
|
||||
scopeReference "scope reference"
|
||||
= identifier:identifier slashFollows:slashFollows? {
|
||||
const id = identifier + (slashFollows ? "/" : "");
|
||||
return annotate(makeReference(id), location());
|
||||
}
|
||||
|
||||
separator
|
||||
= __ "," __
|
||||
/ whitespaceWithNewLine
|
||||
|
||||
// Check whether next character is a slash without consuming input
|
||||
slashFollows
|
||||
// This expression returned `undefined` if successful; we convert to `true`
|
||||
= &"/" {
|
||||
return true;
|
||||
}
|
||||
|
||||
shebang
|
||||
= "#!" [^\n\r]* { return null; }
|
||||
|
||||
shiftExpression
|
||||
= head:additiveExpression tail:(__ @shiftOperator __ @additiveExpression)* {
|
||||
return annotate(tail.reduce(makeBinaryOperation, head), location());
|
||||
}
|
||||
|
||||
shiftOperator
|
||||
= "<<"
|
||||
/ ">>>"
|
||||
/ ">>"
|
||||
|
||||
// A shorthand lambda expression: `=foo(_)`
|
||||
shorthandFunction "lambda function"
|
||||
// Avoid a following equal sign (for an equality)
|
||||
= "=" !"=" __ definition:implicitParenthesesCallExpression {
|
||||
return annotate([ops.lambda, ["_"], definition], location());
|
||||
}
|
||||
/ implicitParenthesesCallExpression
|
||||
|
||||
singleArrow
|
||||
= "→"
|
||||
/ "->"
|
||||
|
||||
singleLineComment
|
||||
= "//" [^\n\r]* { return null; }
|
||||
|
||||
singleQuoteString "single quote string"
|
||||
= "'" chars:singleQuoteStringChar* "'" {
|
||||
return annotate([ops.literal, chars.join("")], location());
|
||||
}
|
||||
|
||||
singleQuoteStringChar
|
||||
= !("'" / newLine) @textChar
|
||||
|
||||
spread
|
||||
= ellipsis __ value:conditionalExpression {
|
||||
return annotate([ops.spread, value], location());
|
||||
}
|
||||
|
||||
stringLiteral "string"
|
||||
= doubleQuoteString
|
||||
/ singleQuoteString
|
||||
/ guillemetString
|
||||
|
||||
// A top-level document defining a template. This is the same as a template
|
||||
// literal, but can contain backticks at the top level.
|
||||
templateDocument "template"
|
||||
= contents:templateDocumentContents {
|
||||
return annotate([ops.lambda, ["_"], contents], location());
|
||||
}
|
||||
|
||||
// Template documents can contain backticks at the top level.
|
||||
templateDocumentChar
|
||||
= !("${") @textChar
|
||||
|
||||
// The contents of a template document containing plain text and substitutions
|
||||
templateDocumentContents
|
||||
= head:templateDocumentText tail:(templateSubstitution templateDocumentText)* {
|
||||
return annotate(makeTemplate(ops.template, head, tail), location());
|
||||
}
|
||||
|
||||
templateDocumentText "template text"
|
||||
= chars:templateDocumentChar* {
|
||||
return chars.join("");
|
||||
}
|
||||
|
||||
// A backtick-quoted template literal
|
||||
templateLiteral "template literal"
|
||||
= "`" contents:templateLiteralContents "`" {
|
||||
return annotate(makeTemplate(ops.template, contents[0], contents[1]), location());
|
||||
}
|
||||
|
||||
templateLiteralChar
|
||||
= !("`" / "${") @textChar
|
||||
|
||||
// The contents of a template literal containing plain text and substitutions
|
||||
templateLiteralContents
|
||||
= head:templateLiteralText tail:(templateSubstitution templateLiteralText)*
|
||||
|
||||
// Plain text in a template literal
|
||||
templateLiteralText
|
||||
= chars:templateLiteralChar* {
|
||||
return chars.join("");
|
||||
}
|
||||
|
||||
// A substitution in a template literal: `${x}`
|
||||
templateSubstitution "template substitution"
|
||||
= "${" expression:expression "}" {
|
||||
return annotate(expression, location());
|
||||
}
|
||||
|
||||
textChar
|
||||
= escapedChar
|
||||
/ .
|
||||
|
||||
// A unary prefix operator: `!x`
|
||||
unaryExpression
|
||||
= operator:unaryOperator __ expression:unaryExpression {
|
||||
return annotate(makeUnaryOperation(operator, expression), location());
|
||||
}
|
||||
/ callExpression
|
||||
|
||||
unaryOperator
|
||||
= "!"
|
||||
/ "+"
|
||||
/ "-"
|
||||
/ "~"
|
||||
|
||||
whitespaceWithNewLine
|
||||
= inlineSpace* comment? newLine __
|
||||
3
node_modules/@weborigami/language/src/compiler/parse.d.ts
generated
vendored
Normal file
3
node_modules/@weborigami/language/src/compiler/parse.d.ts
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Code } from "../../index.ts";
|
||||
|
||||
export function parse(input: string, options: any): Code;
|
||||
4869
node_modules/@weborigami/language/src/compiler/parse.js
generated
vendored
Normal file
4869
node_modules/@weborigami/language/src/compiler/parse.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
363
node_modules/@weborigami/language/src/compiler/parserHelpers.js
generated
vendored
Normal file
363
node_modules/@weborigami/language/src/compiler/parserHelpers.js
generated
vendored
Normal file
@@ -0,0 +1,363 @@
|
||||
import { trailingSlash } from "@weborigami/async-tree";
|
||||
import codeFragment from "../runtime/codeFragment.js";
|
||||
import * as ops from "../runtime/ops.js";
|
||||
|
||||
// Parser helpers
|
||||
|
||||
/** @typedef {import("../../index.ts").Code} Code */
|
||||
|
||||
// Marker for a reference that may be a builtin or a scope reference
|
||||
export const undetermined = Symbol("undetermined");
|
||||
|
||||
const builtinRegex = /^[A-Za-z][A-Za-z0-9]*$/;
|
||||
|
||||
/**
|
||||
* If a parse result is an object that will be evaluated at runtime, attach the
|
||||
* location of the source code that produced it for debugging and error messages.
|
||||
*
|
||||
* @param {Code} code
|
||||
* @param {any} location
|
||||
*/
|
||||
export function annotate(code, location) {
|
||||
if (typeof code === "object" && code !== null && location) {
|
||||
code.location = location;
|
||||
code.source = codeFragment(location);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* The indicated code is being used to define a property named by the given key.
|
||||
* Rewrite any [ops.scope, key] calls to be [ops.inherited, key] to avoid
|
||||
* infinite recursion.
|
||||
*
|
||||
* @param {Code} code
|
||||
* @param {string} key
|
||||
*/
|
||||
function avoidRecursivePropertyCalls(code, key) {
|
||||
if (!(code instanceof Array)) {
|
||||
return code;
|
||||
}
|
||||
/** @type {Code} */
|
||||
let modified;
|
||||
if (
|
||||
code[0] === ops.scope &&
|
||||
trailingSlash.remove(code[1]) === trailingSlash.remove(key)
|
||||
) {
|
||||
// Rewrite to avoid recursion
|
||||
// @ts-ignore
|
||||
modified = [ops.inherited, code[1]];
|
||||
} else if (code[0] === ops.lambda && code[1].includes(key)) {
|
||||
// Lambda that defines the key; don't rewrite
|
||||
return code;
|
||||
} else {
|
||||
// Process any nested code
|
||||
// @ts-ignore
|
||||
modified = code.map((value) => avoidRecursivePropertyCalls(value, key));
|
||||
}
|
||||
annotate(modified, code.location);
|
||||
return modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downgrade a potential builtin reference to a scope reference.
|
||||
*
|
||||
* @param {Code} code
|
||||
*/
|
||||
export function downgradeReference(code) {
|
||||
if (code && code.length === 2 && code[0] === undetermined) {
|
||||
/** @type {Code} */
|
||||
// @ts-ignore
|
||||
const result = [ops.scope, code[1]];
|
||||
annotate(result, code.location);
|
||||
return result;
|
||||
} else {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
// Return true if the code will generate an async object.
|
||||
function isCodeForAsyncObject(code) {
|
||||
if (!(code instanceof Array)) {
|
||||
return false;
|
||||
}
|
||||
if (code[0] !== ops.object) {
|
||||
return false;
|
||||
}
|
||||
// Are any of the properties getters?
|
||||
const entries = code.slice(1);
|
||||
const hasGetter = entries.some(([key, value]) => {
|
||||
return value instanceof Array && value[0] === ops.getter;
|
||||
});
|
||||
return hasGetter;
|
||||
}
|
||||
|
||||
export function makeArray(entries) {
|
||||
let currentEntries = [];
|
||||
const spreads = [];
|
||||
|
||||
for (const value of entries) {
|
||||
if (Array.isArray(value) && value[0] === ops.spread) {
|
||||
if (currentEntries.length > 0) {
|
||||
spreads.push([ops.array, ...currentEntries]);
|
||||
currentEntries = [];
|
||||
}
|
||||
spreads.push(...value.slice(1));
|
||||
} else {
|
||||
currentEntries.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Finish any current entries.
|
||||
if (currentEntries.length > 0) {
|
||||
spreads.push([ops.array, ...currentEntries]);
|
||||
currentEntries = [];
|
||||
}
|
||||
|
||||
if (spreads.length > 1) {
|
||||
return [ops.merge, ...spreads];
|
||||
}
|
||||
if (spreads.length === 1) {
|
||||
return spreads[0];
|
||||
} else {
|
||||
return [ops.array];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a chain of binary operators. The head is the first value, and the tail
|
||||
* is an array of [operator, value] pairs.
|
||||
*
|
||||
* @param {Code} left
|
||||
*/
|
||||
export function makeBinaryOperation(left, [operatorToken, right]) {
|
||||
const operators = {
|
||||
"!=": ops.notEqual,
|
||||
"!==": ops.notStrictEqual,
|
||||
"%": ops.remainder,
|
||||
"&": ops.bitwiseAnd,
|
||||
"*": ops.multiplication,
|
||||
"**": ops.exponentiation,
|
||||
"+": ops.addition,
|
||||
"-": ops.subtraction,
|
||||
"/": ops.division,
|
||||
"<": ops.lessThan,
|
||||
"<<": ops.shiftLeft,
|
||||
"<=": ops.lessThanOrEqual,
|
||||
"==": ops.equal,
|
||||
"===": ops.strictEqual,
|
||||
">": ops.greaterThan,
|
||||
">=": ops.greaterThanOrEqual,
|
||||
">>": ops.shiftRightSigned,
|
||||
">>>": ops.shiftRightUnsigned,
|
||||
"^": ops.bitwiseXor,
|
||||
"|": ops.bitwiseOr,
|
||||
};
|
||||
const op = operators[operatorToken];
|
||||
|
||||
/** @type {Code} */
|
||||
// @ts-ignore
|
||||
const value = [op, left, right];
|
||||
value.location = {
|
||||
source: left.location.source,
|
||||
start: left.location.start,
|
||||
end: right.location.end,
|
||||
};
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Code} target
|
||||
* @param {any[]} args
|
||||
*/
|
||||
export function makeCall(target, args) {
|
||||
if (!(target instanceof Array)) {
|
||||
const error = new SyntaxError(`Can't call this like a function: ${target}`);
|
||||
/** @type {any} */ (error).location = /** @type {any} */ (target).location;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const source = target.location.source;
|
||||
let start = target.location.start;
|
||||
let end = target.location.end;
|
||||
|
||||
let fnCall;
|
||||
if (args[0] === ops.traverse) {
|
||||
let tree = target;
|
||||
|
||||
if (tree[0] === undetermined) {
|
||||
// In a traversal, downgrade ops.builtin references to ops.scope
|
||||
tree = downgradeReference(tree);
|
||||
if (tree[0] === ops.scope && !trailingSlash.has(tree[1])) {
|
||||
// Target didn't parse with a trailing slash; add one
|
||||
tree[1] = trailingSlash.add(tree[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (args.length > 1) {
|
||||
// Regular traverse
|
||||
const keys = args.slice(1);
|
||||
fnCall = [ops.traverse, tree, ...keys];
|
||||
} else {
|
||||
// Traverse without arguments equates to unpack
|
||||
fnCall = [ops.unpack, tree];
|
||||
}
|
||||
} else if (args[0] === ops.template) {
|
||||
// Tagged template
|
||||
fnCall = [upgradeReference(target), ...args.slice(1)];
|
||||
} else {
|
||||
// Function call with explicit or implicit parentheses
|
||||
fnCall = [upgradeReference(target), ...args];
|
||||
}
|
||||
|
||||
// Create a location spanning the newly-constructed function call.
|
||||
if (args instanceof Array) {
|
||||
// @ts-ignore
|
||||
end = args.location?.end ?? args.at(-1)?.location?.end;
|
||||
if (end === undefined) {
|
||||
throw "Internal parser error: no location for function call argument";
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
annotate(fnCall, { start, source, end });
|
||||
|
||||
return fnCall;
|
||||
}
|
||||
|
||||
/**
|
||||
* For functions that short-circuit arguments, we need to defer evaluation of
|
||||
* the arguments until the function is called. Exception: if the argument is a
|
||||
* literal, we leave it alone.
|
||||
*
|
||||
* @param {any[]} args
|
||||
*/
|
||||
export function makeDeferredArguments(args) {
|
||||
return args.map((arg) => {
|
||||
if (arg instanceof Array && arg[0] === ops.literal) {
|
||||
return arg;
|
||||
}
|
||||
const fn = [ops.lambda, [], arg];
|
||||
// @ts-ignore
|
||||
annotate(fn, arg.location);
|
||||
return fn;
|
||||
});
|
||||
}
|
||||
|
||||
export function makeObject(entries, op) {
|
||||
let currentEntries = [];
|
||||
const spreads = [];
|
||||
|
||||
for (let [key, value] of entries) {
|
||||
if (key === ops.spread) {
|
||||
// Spread entry; accumulate
|
||||
if (currentEntries.length > 0) {
|
||||
spreads.push([op, ...currentEntries]);
|
||||
currentEntries = [];
|
||||
}
|
||||
spreads.push(value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
value instanceof Array &&
|
||||
value[0] === ops.getter &&
|
||||
value[1] instanceof Array &&
|
||||
value[1][0] === ops.literal
|
||||
) {
|
||||
// Simplify a getter for a primitive value to a regular property
|
||||
value = value[1];
|
||||
} else if (isCodeForAsyncObject(value)) {
|
||||
// Add a trailing slash to key to indicate value is a subtree
|
||||
key = key + "/";
|
||||
}
|
||||
|
||||
currentEntries.push([key, value]);
|
||||
}
|
||||
|
||||
// Finish any current entries.
|
||||
if (currentEntries.length > 0) {
|
||||
spreads.push([op, ...currentEntries]);
|
||||
currentEntries = [];
|
||||
}
|
||||
|
||||
if (spreads.length > 1) {
|
||||
return [ops.merge, ...spreads];
|
||||
}
|
||||
if (spreads.length === 1) {
|
||||
return spreads[0];
|
||||
} else {
|
||||
return [op];
|
||||
}
|
||||
}
|
||||
|
||||
// Similar to a function call, but the order is reversed.
|
||||
export function makePipeline(arg, fn) {
|
||||
const upgraded = upgradeReference(fn);
|
||||
const result = makeCall(upgraded, [arg]);
|
||||
const source = fn.location.source;
|
||||
let start = arg.location.start;
|
||||
let end = fn.location.end;
|
||||
// @ts-ignore
|
||||
annotate(result, { start, source, end });
|
||||
return result;
|
||||
}
|
||||
|
||||
// Define a property on an object.
|
||||
export function makeProperty(key, value) {
|
||||
const modified = avoidRecursivePropertyCalls(value, key);
|
||||
return [key, modified];
|
||||
}
|
||||
|
||||
export function makeReference(identifier) {
|
||||
// We can't know for sure that an identifier is a builtin reference until we
|
||||
// see whether it's being called as a function.
|
||||
let op;
|
||||
if (builtinRegex.test(identifier)) {
|
||||
op = identifier.endsWith(":")
|
||||
? // Namespace is always a builtin reference
|
||||
ops.builtin
|
||||
: undetermined;
|
||||
} else {
|
||||
op = ops.scope;
|
||||
}
|
||||
return [op, identifier];
|
||||
}
|
||||
|
||||
export function makeTemplate(op, head, tail) {
|
||||
const strings = [head];
|
||||
const values = [];
|
||||
for (const [value, string] of tail) {
|
||||
values.push([ops.concat, value]);
|
||||
strings.push(string);
|
||||
}
|
||||
return [op, [ops.literal, strings], ...values];
|
||||
}
|
||||
|
||||
export function makeUnaryOperation(operator, value) {
|
||||
const operators = {
|
||||
"!": ops.logicalNot,
|
||||
"+": ops.unaryPlus,
|
||||
"-": ops.unaryMinus,
|
||||
"~": ops.bitwiseNot,
|
||||
};
|
||||
return [operators[operator], value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade a potential builtin reference to an actual builtin reference.
|
||||
*
|
||||
* @param {Code} code
|
||||
*/
|
||||
export function upgradeReference(code) {
|
||||
if (code.length === 2 && code[0] === undetermined) {
|
||||
/** @type {Code} */
|
||||
// @ts-ignore
|
||||
const result = [ops.builtin, code[1]];
|
||||
annotate(result, code.location);
|
||||
return result;
|
||||
} else {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
9
node_modules/@weborigami/language/src/runtime/EventTargetMixin.d.ts
generated
vendored
Normal file
9
node_modules/@weborigami/language/src/runtime/EventTargetMixin.d.ts
generated
vendored
Normal 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;
|
||||
117
node_modules/@weborigami/language/src/runtime/EventTargetMixin.js
generated
vendored
Normal file
117
node_modules/@weborigami/language/src/runtime/EventTargetMixin.js
generated
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
5
node_modules/@weborigami/language/src/runtime/HandleExtensionsTransform.d.ts
generated
vendored
Normal file
5
node_modules/@weborigami/language/src/runtime/HandleExtensionsTransform.d.ts
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Mixin } from "../../index.ts";
|
||||
|
||||
declare const HandleExtensionsTransform: Mixin<{}>;
|
||||
|
||||
export default HandleExtensionsTransform;
|
||||
17
node_modules/@weborigami/language/src/runtime/HandleExtensionsTransform.js
generated
vendored
Normal file
17
node_modules/@weborigami/language/src/runtime/HandleExtensionsTransform.js
generated
vendored
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
5
node_modules/@weborigami/language/src/runtime/ImportModulesMixin.d.ts
generated
vendored
Normal file
5
node_modules/@weborigami/language/src/runtime/ImportModulesMixin.d.ts
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Mixin } from "../../index.ts";
|
||||
|
||||
declare const ImportModulesMixin: Mixin<{}>;
|
||||
|
||||
export default ImportModulesMixin;
|
||||
58
node_modules/@weborigami/language/src/runtime/ImportModulesMixin.js
generated
vendored
Normal file
58
node_modules/@weborigami/language/src/runtime/ImportModulesMixin.js
generated
vendored
Normal 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 };
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
5
node_modules/@weborigami/language/src/runtime/InvokeFunctionsTransform.d.ts
generated
vendored
Normal file
5
node_modules/@weborigami/language/src/runtime/InvokeFunctionsTransform.d.ts
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Mixin } from "../../index.ts";
|
||||
|
||||
declare const InvokeFunctionsTransform: Mixin<{}>;
|
||||
|
||||
export default InvokeFunctionsTransform;
|
||||
25
node_modules/@weborigami/language/src/runtime/InvokeFunctionsTransform.js
generated
vendored
Normal file
25
node_modules/@weborigami/language/src/runtime/InvokeFunctionsTransform.js
generated
vendored
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
11
node_modules/@weborigami/language/src/runtime/OrigamiFiles.d.ts
generated
vendored
Normal file
11
node_modules/@weborigami/language/src/runtime/OrigamiFiles.d.ts
generated
vendored
Normal 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)))
|
||||
)
|
||||
) {}
|
||||
9
node_modules/@weborigami/language/src/runtime/OrigamiFiles.js
generated
vendored
Normal file
9
node_modules/@weborigami/language/src/runtime/OrigamiFiles.js
generated
vendored
Normal 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)))
|
||||
) {}
|
||||
1
node_modules/@weborigami/language/src/runtime/ReadMe.md
generated
vendored
Normal file
1
node_modules/@weborigami/language/src/runtime/ReadMe.md
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Modules necessary to evaluate Origami expressions
|
||||
6
node_modules/@weborigami/language/src/runtime/TreeEvent.js
generated
vendored
Normal file
6
node_modules/@weborigami/language/src/runtime/TreeEvent.js
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
export default class TreeEvent extends Event {
|
||||
constructor(type, options = {}) {
|
||||
super(type, options);
|
||||
this.options = options;
|
||||
}
|
||||
}
|
||||
5
node_modules/@weborigami/language/src/runtime/WatchFilesMixin.d.ts
generated
vendored
Normal file
5
node_modules/@weborigami/language/src/runtime/WatchFilesMixin.d.ts
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Mixin } from "../../index.ts";
|
||||
|
||||
declare const WatchFilesMixin: Mixin<{}>;
|
||||
|
||||
export default WatchFilesMixin;
|
||||
59
node_modules/@weborigami/language/src/runtime/WatchFilesMixin.js
generated
vendored
Normal file
59
node_modules/@weborigami/language/src/runtime/WatchFilesMixin.js
generated
vendored
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
19
node_modules/@weborigami/language/src/runtime/codeFragment.js
generated
vendored
Normal file
19
node_modules/@weborigami/language/src/runtime/codeFragment.js
generated
vendored
Normal 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
104
node_modules/@weborigami/language/src/runtime/errors.js
generated
vendored
Normal 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);
|
||||
}
|
||||
116
node_modules/@weborigami/language/src/runtime/evaluate.js
generated
vendored
Normal file
116
node_modules/@weborigami/language/src/runtime/evaluate.js
generated
vendored
Normal 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;
|
||||
}
|
||||
33
node_modules/@weborigami/language/src/runtime/expressionFunction.js
generated
vendored
Normal file
33
node_modules/@weborigami/language/src/runtime/expressionFunction.js
generated
vendored
Normal 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;
|
||||
}
|
||||
120
node_modules/@weborigami/language/src/runtime/expressionObject.js
generated
vendored
Normal file
120
node_modules/@weborigami/language/src/runtime/expressionObject.js
generated
vendored
Normal 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;
|
||||
}
|
||||
24
node_modules/@weborigami/language/src/runtime/functionResultsMap.js
generated
vendored
Normal file
24
node_modules/@weborigami/language/src/runtime/functionResultsMap.js
generated
vendored
Normal 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;
|
||||
},
|
||||
});
|
||||
}
|
||||
110
node_modules/@weborigami/language/src/runtime/handlers.js
generated
vendored
Normal file
110
node_modules/@weborigami/language/src/runtime/handlers.js
generated
vendored
Normal 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;
|
||||
}
|
||||
15
node_modules/@weborigami/language/src/runtime/internal.js
generated
vendored
Normal file
15
node_modules/@weborigami/language/src/runtime/internal.js
generated
vendored
Normal 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";
|
||||
43
node_modules/@weborigami/language/src/runtime/mergeTrees.js
generated
vendored
Normal file
43
node_modules/@weborigami/language/src/runtime/mergeTrees.js
generated
vendored
Normal 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
477
node_modules/@weborigami/language/src/runtime/ops.js
generated
vendored
Normal 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;
|
||||
}
|
||||
3
node_modules/@weborigami/language/src/runtime/symbols.js
generated
vendored
Normal file
3
node_modules/@weborigami/language/src/runtime/symbols.js
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export const codeSymbol = Symbol("code");
|
||||
export const scopeSymbol = Symbol("scope");
|
||||
export const sourceSymbol = Symbol("source");
|
||||
9
node_modules/@weborigami/language/src/runtime/taggedTemplate.js
generated
vendored
Normal file
9
node_modules/@weborigami/language/src/runtime/taggedTemplate.js
generated
vendored
Normal 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
71
node_modules/@weborigami/language/src/runtime/typos.js
generated
vendored
Normal 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));
|
||||
}
|
||||
1
node_modules/@weborigami/language/test/cases/ReadMe.md
generated
vendored
Normal file
1
node_modules/@weborigami/language/test/cases/ReadMe.md
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
This folder defines expression tests in YAML files so that we can programmatically test the evaluation of the expressions in both JavaScript and Origami.
|
||||
101
node_modules/@weborigami/language/test/cases/conditionalExpression.yaml
generated
vendored
Normal file
101
node_modules/@weborigami/language/test/cases/conditionalExpression.yaml
generated
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
# Conditional (ternary) expression tests
|
||||
|
||||
- source: "true ? 42 : 0"
|
||||
expected: 42
|
||||
description: "Condition is true, evaluates and returns the first operand"
|
||||
|
||||
- source: "false ? 42 : 0"
|
||||
expected: 0
|
||||
description: "Condition is false, evaluates and returns the second operand"
|
||||
|
||||
- source: "1 ? 'yes' : 'no'"
|
||||
expected: "yes"
|
||||
description: "Truthy condition with string operands"
|
||||
|
||||
- source: "0 ? 'yes' : 'no'"
|
||||
expected: "no"
|
||||
description: "Falsy condition with string operands"
|
||||
|
||||
- source: "'non-empty' ? 1 : 2"
|
||||
expected: 1
|
||||
description: "Truthy string condition with numeric operands"
|
||||
|
||||
- source: "'' ? 1 : 2"
|
||||
expected: 2
|
||||
description: "Falsy string condition with numeric operands"
|
||||
|
||||
- source: "null ? 'a' : 'b'"
|
||||
expected: "b"
|
||||
description: "Falsy null condition"
|
||||
|
||||
- source: "undefined ? 'a' : 'b'"
|
||||
expected: "b"
|
||||
description: "Falsy undefined condition"
|
||||
|
||||
- source: "NaN ? 'a' : 'b'"
|
||||
expected: "b"
|
||||
description: "Falsy NaN condition"
|
||||
|
||||
- source: "42 ? true : false"
|
||||
expected: true
|
||||
description: "Truthy numeric condition with boolean operands"
|
||||
|
||||
- source: "0 ? true : false"
|
||||
expected: false
|
||||
description: "Falsy numeric condition with boolean operands"
|
||||
|
||||
- source: "[] ? 'array' : 'no array'"
|
||||
expected: "array"
|
||||
description: "Truthy array condition"
|
||||
|
||||
- source: "{} ? 'object' : 'no object'"
|
||||
expected: "object"
|
||||
description: "Truthy object condition"
|
||||
|
||||
- source: "false ? null : undefined"
|
||||
expected: __undefined__
|
||||
description: "Condition is false, returns undefined"
|
||||
|
||||
- source: "null ? null : null"
|
||||
expected: __null__
|
||||
description: "Condition is falsy, returns null"
|
||||
|
||||
- source: "true ? NaN : 42"
|
||||
expected: __NaN__
|
||||
description: "Condition is true, evaluates and returns NaN"
|
||||
|
||||
- source: "(true ? 1 : 2) ? 3 : 4"
|
||||
expected: 3
|
||||
description: "Nested ternary where first expression evaluates to 1, which is truthy"
|
||||
|
||||
- source: "(false ? 1 : 2) ? 3 : 4"
|
||||
expected: 3
|
||||
description: "Nested ternary where first expression evaluates to 2, which is truthy"
|
||||
|
||||
- source: "(false ? 1 : 0) ? 3 : 4"
|
||||
expected: 4
|
||||
description: "Nested ternary where first expression evaluates to 0, which is falsy"
|
||||
|
||||
- source: "true ? (false ? 10 : 20) : 30"
|
||||
expected: 20
|
||||
description: "Nested ternary in the true branch of outer ternary"
|
||||
|
||||
- source: "false ? (false ? 10 : 20) : 30"
|
||||
expected: 30
|
||||
description: "Nested ternary in the false branch of outer ternary"
|
||||
|
||||
# - source: "'truthy' ? 1 + 2 : 3 + 4"
|
||||
# expected: 3
|
||||
# description: "Evaluates and returns the true branch with an arithmetic expression"
|
||||
|
||||
# - source: "'' ? 1 + 2 : 3 + 4"
|
||||
# expected: 7
|
||||
# description: "Evaluates and returns the false branch with an arithmetic expression"
|
||||
|
||||
- source: "undefined ? undefined : null"
|
||||
expected: __null__
|
||||
description: "Condition is falsy, returns null"
|
||||
|
||||
- source: "null ? undefined : undefined"
|
||||
expected: __undefined__
|
||||
description: "Condition is falsy, returns undefined"
|
||||
146
node_modules/@weborigami/language/test/cases/logicalAndExpression.yaml
generated
vendored
Normal file
146
node_modules/@weborigami/language/test/cases/logicalAndExpression.yaml
generated
vendored
Normal file
@@ -0,0 +1,146 @@
|
||||
# Logical AND expression tests
|
||||
|
||||
- source: "true && true"
|
||||
expected: true
|
||||
description: "Both operands are true"
|
||||
|
||||
- source: "true && false"
|
||||
expected: false
|
||||
description: "First operand is true, second is false"
|
||||
|
||||
- source: "false && true"
|
||||
expected: false
|
||||
description: "First operand is false, second is true"
|
||||
|
||||
- source: "false && false"
|
||||
expected: false
|
||||
description: "Both operands are false"
|
||||
|
||||
- source: "false && (1 / 0)"
|
||||
expected: false
|
||||
description: "Short-circuit evaluation: first operand false, second not evaluated"
|
||||
|
||||
- source: "true && 42"
|
||||
expected: 42
|
||||
description: "Short-circuit evaluation: first operand true, evaluates second"
|
||||
|
||||
- source: "0 && true"
|
||||
expected: 0
|
||||
description: "Short-circuiting with falsy value (0)"
|
||||
|
||||
- source: "true && 'string'"
|
||||
expected: "string"
|
||||
description: "Truthy value with string"
|
||||
|
||||
- source: "false && 'string'"
|
||||
expected: false
|
||||
description: "Falsy value with string"
|
||||
|
||||
- source: "1 && 0"
|
||||
expected: 0
|
||||
description: "Truthy numeric value with falsy numeric value"
|
||||
|
||||
- source: "0 && 1"
|
||||
expected: 0
|
||||
description: "Falsy numeric value with truthy numeric value"
|
||||
|
||||
- source: "'' && 'non-empty string'"
|
||||
expected: ""
|
||||
description: "Falsy string value with truthy string"
|
||||
|
||||
- source: "'non-empty string' && ''"
|
||||
expected: ""
|
||||
description: "Truthy string with falsy string"
|
||||
|
||||
- source: "{} && true"
|
||||
expected: true
|
||||
description: "Empty object as first operand"
|
||||
|
||||
- source: "true && {}"
|
||||
expected: {}
|
||||
description: "Empty object as second operand"
|
||||
|
||||
- source: "[] && true"
|
||||
expected: true
|
||||
description: "Array as first operand"
|
||||
|
||||
- source: "true && []"
|
||||
expected: []
|
||||
description: "Array as second operand"
|
||||
|
||||
- source: "null && true"
|
||||
expected: null
|
||||
description: "Null as first operand"
|
||||
|
||||
- source: "true && null"
|
||||
expected: null
|
||||
description: "Null as second operand"
|
||||
|
||||
- source: "undefined && true"
|
||||
expected: __undefined__
|
||||
description: "Undefined as first operand"
|
||||
|
||||
- source: "true && undefined"
|
||||
expected: __undefined__
|
||||
description: "Undefined as second operand"
|
||||
|
||||
- source: "NaN && true"
|
||||
expected: __NaN__
|
||||
description: "NaN as first operand"
|
||||
|
||||
- source: "true && NaN"
|
||||
expected: __NaN__
|
||||
description: "NaN as second operand"
|
||||
|
||||
- source: "(true && false) && true"
|
||||
expected: false
|
||||
description: "Nested logical ANDs with a false in the middle"
|
||||
|
||||
- source: "(true && true) && true"
|
||||
expected: true
|
||||
description: "Nested logical ANDs with all true"
|
||||
|
||||
- source: "true && (true && false)"
|
||||
expected: false
|
||||
description: "Nested logical ANDs with false in inner"
|
||||
|
||||
- source: "(true && (false && true))"
|
||||
expected: false
|
||||
description: "Complex nesting with false at inner-most"
|
||||
|
||||
# TODO: Uncomment when we can do math
|
||||
# - source: "true && (1 + 1 === 2)"
|
||||
# expected: true
|
||||
# description: "Combines logical AND with equality comparison"
|
||||
|
||||
# - source: "false && (5 > 2)"
|
||||
# expected: false
|
||||
# description: "Logical AND with greater-than comparison"
|
||||
|
||||
- source: "true && (3 || 0)"
|
||||
expected: 3
|
||||
description: "Logical AND with logical OR"
|
||||
|
||||
- source: "true && (0 || 3)"
|
||||
expected: 3
|
||||
description: "Logical AND with logical OR and falsy values"
|
||||
|
||||
- source: "'' && false"
|
||||
expected: ""
|
||||
description: "Falsy string and false"
|
||||
|
||||
- source: "false && ''"
|
||||
expected: false
|
||||
description: "False and falsy string"
|
||||
|
||||
- source: "undefined && null"
|
||||
expected: __undefined__
|
||||
description: "Undefined and null"
|
||||
|
||||
- source: "null && undefined"
|
||||
expected: null
|
||||
description: "Null and undefined"
|
||||
|
||||
- source: "(false && true) && undefined"
|
||||
expected: false
|
||||
description: "Short-circuiting nested AND with undefined"
|
||||
145
node_modules/@weborigami/language/test/cases/logicalOrExpression.yaml
generated
vendored
Normal file
145
node_modules/@weborigami/language/test/cases/logicalOrExpression.yaml
generated
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
# Logical OR expression tests
|
||||
|
||||
- source: "true || true"
|
||||
expected: true
|
||||
description: "Both operands are true"
|
||||
|
||||
- source: "true || false"
|
||||
expected: true
|
||||
description: "First operand is true, second is false"
|
||||
|
||||
- source: "false || true"
|
||||
expected: true
|
||||
description: "First operand is false, second is true"
|
||||
|
||||
- source: "false || false"
|
||||
expected: false
|
||||
description: "Both operands are false"
|
||||
|
||||
# - source: "true || (1 / 0)"
|
||||
# expected: true
|
||||
# description: "Short-circuit evaluation: first operand true, second not evaluated"
|
||||
|
||||
- source: "false || 42"
|
||||
expected: 42
|
||||
description: "Short-circuit evaluation: first operand false, evaluates second"
|
||||
|
||||
- source: "0 || true"
|
||||
expected: true
|
||||
description: "Falsy value (0) with truthy second operand"
|
||||
|
||||
- source: "true || 'string'"
|
||||
expected: true
|
||||
description: "Truthy first operand, string second operand not evaluated"
|
||||
|
||||
- source: "false || 'string'"
|
||||
expected: "string"
|
||||
description: "Falsy first operand, evaluates string second operand"
|
||||
|
||||
- source: "1 || 0"
|
||||
expected: 1
|
||||
description: "Truthy numeric value with falsy numeric value"
|
||||
|
||||
- source: "0 || 1"
|
||||
expected: 1
|
||||
description: "Falsy numeric value with truthy numeric value"
|
||||
|
||||
- source: "'' || 'non-empty string'"
|
||||
expected: "non-empty string"
|
||||
description: "Falsy string value with truthy string"
|
||||
|
||||
- source: "'non-empty string' || ''"
|
||||
expected: "non-empty string"
|
||||
description: "Truthy string with falsy string"
|
||||
|
||||
- source: "{} || true"
|
||||
expected: {}
|
||||
description: "Empty object as first operand"
|
||||
|
||||
- source: "true || {}"
|
||||
expected: true
|
||||
description: "True as first operand, object not evaluated"
|
||||
|
||||
- source: "[] || true"
|
||||
expected: []
|
||||
description: "Array as first operand"
|
||||
|
||||
- source: "true || []"
|
||||
expected: true
|
||||
description: "True as first operand, array not evaluated"
|
||||
|
||||
- source: "null || true"
|
||||
expected: true
|
||||
description: "Null as first operand"
|
||||
|
||||
- source: "true || null"
|
||||
expected: true
|
||||
description: "True as first operand, null not evaluated"
|
||||
|
||||
- source: "undefined || true"
|
||||
expected: true
|
||||
description: "Undefined as first operand"
|
||||
|
||||
- source: "true || undefined"
|
||||
expected: true
|
||||
description: "True as first operand, undefined not evaluated"
|
||||
|
||||
- source: "NaN || true"
|
||||
expected: true
|
||||
description: "NaN as first operand"
|
||||
|
||||
- source: "true || NaN"
|
||||
expected: true
|
||||
description: "True as first operand, NaN not evaluated"
|
||||
|
||||
- source: "(false || true) || false"
|
||||
expected: true
|
||||
description: "Nested logical ORs with a true in the middle"
|
||||
|
||||
- source: "(false || false) || true"
|
||||
expected: true
|
||||
description: "Nested logical ORs with a true at the end"
|
||||
|
||||
- source: "false || (false || true)"
|
||||
expected: true
|
||||
description: "Nested logical ORs with true in inner"
|
||||
|
||||
- source: "(false || (true || false))"
|
||||
expected: true
|
||||
description: "Complex nesting with true at inner-most"
|
||||
|
||||
# - source: "true || (1 + 1 === 2)"
|
||||
# expected: true
|
||||
# description: "Combines logical OR with equality comparison"
|
||||
|
||||
# - source: "false || (5 > 2)"
|
||||
# expected: true
|
||||
# description: "Logical OR with greater-than comparison"
|
||||
|
||||
- source: "false || (3 && 0)"
|
||||
expected: 0
|
||||
description: "Logical OR with logical AND and falsy result"
|
||||
|
||||
- source: "false || (0 && 3)"
|
||||
expected: 0
|
||||
description: "Logical OR with logical AND and falsy first operand"
|
||||
|
||||
- source: "'' || false"
|
||||
expected: false
|
||||
description: "Falsy string and false"
|
||||
|
||||
- source: "false || ''"
|
||||
expected: ""
|
||||
description: "False and falsy string"
|
||||
|
||||
- source: "undefined || null"
|
||||
expected: __null__
|
||||
description: "Undefined and null"
|
||||
|
||||
- source: "null || undefined"
|
||||
expected: __undefined__
|
||||
description: "Null and undefined"
|
||||
|
||||
- source: "(true || false) || undefined"
|
||||
expected: true
|
||||
description: "Short-circuiting nested OR with undefined"
|
||||
105
node_modules/@weborigami/language/test/cases/nullishCoalescingExpression.yaml
generated
vendored
Normal file
105
node_modules/@weborigami/language/test/cases/nullishCoalescingExpression.yaml
generated
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
# Nullish coalescing expression tests
|
||||
|
||||
- source: "null ?? 42"
|
||||
expected: 42
|
||||
description: "Left operand is null, returns right operand"
|
||||
|
||||
- source: "undefined ?? 42"
|
||||
expected: 42
|
||||
description: "Left operand is undefined, returns right operand"
|
||||
|
||||
- source: "0 ?? 42"
|
||||
expected: 0
|
||||
description: "Left operand is 0 (falsy but not nullish), returns left operand"
|
||||
|
||||
- source: "'' ?? 'default'"
|
||||
expected: ""
|
||||
description: "Left operand is an empty string (falsy but not nullish), returns left operand"
|
||||
|
||||
- source: "false ?? true"
|
||||
expected: false
|
||||
description: "Left operand is false (falsy but not nullish), returns left operand"
|
||||
|
||||
- source: "42 ?? 0"
|
||||
expected: 42
|
||||
description: "Left operand is a non-nullish truthy value, returns left operand"
|
||||
|
||||
- source: "null ?? undefined"
|
||||
expected: __undefined__
|
||||
description: "Left operand is null, returns right operand which is undefined"
|
||||
|
||||
- source: "undefined ?? null"
|
||||
expected: __null__
|
||||
description: "Left operand is undefined, returns right operand which is null"
|
||||
|
||||
- source: "NaN ?? 42"
|
||||
expected: __NaN__
|
||||
description: "Left operand is NaN (not nullish), returns left operand"
|
||||
|
||||
- source: "[] ?? 'default'"
|
||||
expected: []
|
||||
description: "Left operand is an empty array (not nullish), returns left operand"
|
||||
|
||||
- source: "{} ?? 'default'"
|
||||
expected: {}
|
||||
description: "Left operand is an empty object (not nullish), returns left operand"
|
||||
|
||||
- source: "(null ?? 42) ?? 50"
|
||||
expected: 42
|
||||
description: "Nested nullish coalescing, first nullish operand replaced, second ignored"
|
||||
|
||||
- source: "(undefined ?? null) ?? 'fallback'"
|
||||
expected: fallback
|
||||
description: "Nested nullish coalescing"
|
||||
|
||||
- source: "(0 ?? null) ?? 'fallback'"
|
||||
expected: 0
|
||||
description: "Nested nullish coalescing with falsy but non-nullish value"
|
||||
|
||||
- source: "null ?? (undefined ?? 42)"
|
||||
expected: 42
|
||||
description: "Nullish coalescing in the right operand"
|
||||
|
||||
- source: "null ?? null ?? null ?? 'fallback'"
|
||||
expected: "fallback"
|
||||
description: "Chained nullish coalescing, resolves to the last non-nullish value"
|
||||
|
||||
- source: "null ?? (false ?? 'default')"
|
||||
expected: false
|
||||
description: "Right operand evaluates to non-nullish falsy value"
|
||||
|
||||
- source: "null ?? (true ?? 'default')"
|
||||
expected: true
|
||||
description: "Right operand evaluates to truthy value"
|
||||
|
||||
- source: "42 ?? (null ?? 0)"
|
||||
expected: 42
|
||||
description: "Left operand is not nullish, ignores right operand"
|
||||
|
||||
- source: "undefined ?? null ?? 'value'"
|
||||
expected: "value"
|
||||
description: "Chained nullish coalescing with undefined and null"
|
||||
|
||||
- source: "(NaN ?? null) ?? 42"
|
||||
expected: __NaN__
|
||||
description: "Left operand is NaN, not nullish, returns NaN"
|
||||
|
||||
- source: "(undefined ?? NaN) ?? 42"
|
||||
expected: __NaN__
|
||||
description: "Right operand resolves to NaN"
|
||||
|
||||
- source: "null ?? 'default' ?? 42"
|
||||
expected: "default"
|
||||
description: "Chained nullish coalescing, resolves to first non-nullish value"
|
||||
|
||||
- source: "'' ?? 'default' ?? 42"
|
||||
expected: ""
|
||||
description: "Falsy but non-nullish value, returns left operand"
|
||||
|
||||
- source: "null ?? undefined ?? NaN"
|
||||
expected: __NaN__
|
||||
description: "Chained nullish coalescing, resolves to NaN as the first non-nullish value"
|
||||
|
||||
- source: "(null ?? null) ?? undefined"
|
||||
expected: __undefined__
|
||||
description: "Nested nullish coalescing resolves to undefined"
|
||||
117
node_modules/@weborigami/language/test/compiler/compile.test.js
generated
vendored
Normal file
117
node_modules/@weborigami/language/test/compiler/compile.test.js
generated
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
import { ObjectTree, symbols, Tree } from "@weborigami/async-tree";
|
||||
import assert from "node:assert";
|
||||
import { describe, test } from "node:test";
|
||||
import * as compile from "../../src/compiler/compile.js";
|
||||
import { ops } from "../../src/runtime/internal.js";
|
||||
import { stripCodeLocations } from "./stripCodeLocations.js";
|
||||
|
||||
const shared = new ObjectTree({
|
||||
greet: (name) => `Hello, ${name}!`,
|
||||
name: "Alice",
|
||||
});
|
||||
|
||||
describe.only("compile", () => {
|
||||
test("array", async () => {
|
||||
await assertCompile("[]", []);
|
||||
await assertCompile("[ 1, 2, 3, ]", [1, 2, 3]);
|
||||
await assertCompile("[\n'a'\n'b'\n'c'\n]", ["a", "b", "c"]);
|
||||
});
|
||||
|
||||
test("functionComposition", async () => {
|
||||
await assertCompile("greet()", "Hello, undefined!");
|
||||
await assertCompile("greet(name)", "Hello, Alice!");
|
||||
await assertCompile("greet 'world'", "Hello, world!");
|
||||
});
|
||||
|
||||
test("tree", async () => {
|
||||
const fn = compile.expression("{ message = greet(name) }");
|
||||
const tree = await fn.call(null);
|
||||
tree[symbols.parent] = shared;
|
||||
assert.deepEqual(await Tree.plain(tree), {
|
||||
message: "Hello, Alice!",
|
||||
});
|
||||
});
|
||||
|
||||
test("number", async () => {
|
||||
await assertCompile("1", 1);
|
||||
await assertCompile("3.14159", 3.14159);
|
||||
await assertCompile("-1", -1);
|
||||
});
|
||||
|
||||
test("sync object", async () => {
|
||||
await assertCompile("{a:1, b:2}", { a: 1, b: 2 });
|
||||
await assertCompile("{ a: { b: { c: 0 } } }", { a: { b: { c: 0 } } });
|
||||
});
|
||||
|
||||
test("async object", async () => {
|
||||
const fn = compile.expression("{ a: { b = name }}");
|
||||
const object = await fn.call(shared);
|
||||
assert.deepEqual(await object["a/"].b, "Alice");
|
||||
});
|
||||
|
||||
test("templateDocument", async () => {
|
||||
const fn = compile.templateDocument("Documents can contain ` backticks");
|
||||
const templateFn = await fn.call(shared);
|
||||
const value = await templateFn.call(null);
|
||||
assert.deepEqual(value, "Documents can contain ` backticks");
|
||||
});
|
||||
|
||||
test("templateLiteral", async () => {
|
||||
await assertCompile("`Hello, ${name}!`", "Hello, Alice!");
|
||||
await assertCompile(
|
||||
"`escape characters with \\`backslash\\``",
|
||||
"escape characters with `backslash`"
|
||||
);
|
||||
});
|
||||
|
||||
test("tagged template string array is identical across calls", async () => {
|
||||
let saved;
|
||||
const scope = new ObjectTree({
|
||||
tag: (strings, ...values) => {
|
||||
assert.deepEqual(strings, ["Hello, ", "!"]);
|
||||
if (saved) {
|
||||
assert.equal(strings, saved);
|
||||
} else {
|
||||
saved = strings;
|
||||
}
|
||||
return strings[0] + values[0] + strings[1];
|
||||
},
|
||||
});
|
||||
const program = compile.expression("=tag`Hello, ${_}!`");
|
||||
const lambda = await program.call(scope);
|
||||
const alice = await lambda("Alice");
|
||||
assert.equal(alice, "Hello, Alice!");
|
||||
const bob = await lambda("Bob");
|
||||
assert.equal(bob, "Hello, Bob!");
|
||||
});
|
||||
|
||||
test.only("converts non-local ops.scope calls to ops.external", async () => {
|
||||
const expression = `
|
||||
(name) => {
|
||||
a: 1
|
||||
b: a // local, should be left as ops.scope
|
||||
c: nonLocal // non-local, should be converted to ops.cache
|
||||
d: name // local, should be left as ops.scope
|
||||
}
|
||||
`;
|
||||
const fn = compile.expression(expression);
|
||||
const code = fn.code;
|
||||
assert.deepEqual(stripCodeLocations(code), [
|
||||
ops.lambda,
|
||||
["name"],
|
||||
[
|
||||
ops.object,
|
||||
["a", [ops.literal, 1]],
|
||||
["b", [ops.scope, "a"]],
|
||||
["c", [ops.external, "nonLocal", {}]],
|
||||
["d", [ops.scope, "name"]],
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
async function assertCompile(text, expected) {
|
||||
const fn = compile.expression(text);
|
||||
const result = await fn.call(shared);
|
||||
assert.deepEqual(result, expected);
|
||||
}
|
||||
1069
node_modules/@weborigami/language/test/compiler/parse.test.js
generated
vendored
Normal file
1069
node_modules/@weborigami/language/test/compiler/parse.test.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
18
node_modules/@weborigami/language/test/compiler/stripCodeLocations.js
generated
vendored
Normal file
18
node_modules/@weborigami/language/test/compiler/stripCodeLocations.js
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
import { isPlainObject } from "@weborigami/async-tree";
|
||||
|
||||
// For comparison purposes, strip the `location` property added by the parser.
|
||||
export function stripCodeLocations(parseResult) {
|
||||
if (Array.isArray(parseResult)) {
|
||||
return parseResult.map(stripCodeLocations);
|
||||
} else if (isPlainObject(parseResult)) {
|
||||
const result = {};
|
||||
for (const key in parseResult) {
|
||||
if (key !== "location") {
|
||||
result[key] = stripCodeLocations(parseResult[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
return parseResult;
|
||||
}
|
||||
}
|
||||
58
node_modules/@weborigami/language/test/generated/conditionalExpression.test.js
generated
vendored
Normal file
58
node_modules/@weborigami/language/test/generated/conditionalExpression.test.js
generated
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
// Generated tests -- do not edit directly
|
||||
// @ts-nocheck
|
||||
|
||||
import assert from "node:assert";
|
||||
import { describe } from "node:test";
|
||||
import oriEval from "../generator/oriEval.js";
|
||||
|
||||
describe("conditionalExpression - JavaScript", () => {
|
||||
assert.strictEqual(true ? 42 : 0, 42, "Condition is true, evaluates and returns the first operand");
|
||||
assert.strictEqual(false ? 42 : 0, 0, "Condition is false, evaluates and returns the second operand");
|
||||
assert.strictEqual(1 ? 'yes' : 'no', "yes", "Truthy condition with string operands");
|
||||
assert.strictEqual(0 ? 'yes' : 'no', "no", "Falsy condition with string operands");
|
||||
assert.strictEqual('non-empty' ? 1 : 2, 1, "Truthy string condition with numeric operands");
|
||||
assert.strictEqual('' ? 1 : 2, 2, "Falsy string condition with numeric operands");
|
||||
assert.strictEqual(null ? 'a' : 'b', "b", "Falsy null condition");
|
||||
assert.strictEqual(undefined ? 'a' : 'b', "b", "Falsy undefined condition");
|
||||
assert.strictEqual(NaN ? 'a' : 'b', "b", "Falsy NaN condition");
|
||||
assert.strictEqual(42 ? true : false, true, "Truthy numeric condition with boolean operands");
|
||||
assert.strictEqual(0 ? true : false, false, "Falsy numeric condition with boolean operands");
|
||||
assert.strictEqual([] ? 'array' : 'no array', "array", "Truthy array condition");
|
||||
assert.strictEqual({} ? 'object' : 'no object', "object", "Truthy object condition");
|
||||
assert.strictEqual(false ? null : undefined, undefined, "Condition is false, returns undefined");
|
||||
assert.deepEqual(null ? null : null, null, "Condition is falsy, returns null");
|
||||
assert.strictEqual(true ? NaN : 42, NaN, "Condition is true, evaluates and returns NaN");
|
||||
assert.strictEqual((true ? 1 : 2) ? 3 : 4, 3, "Nested ternary where first expression evaluates to 1, which is truthy");
|
||||
assert.strictEqual((false ? 1 : 2) ? 3 : 4, 3, "Nested ternary where first expression evaluates to 2, which is truthy");
|
||||
assert.strictEqual((false ? 1 : 0) ? 3 : 4, 4, "Nested ternary where first expression evaluates to 0, which is falsy");
|
||||
assert.strictEqual(true ? (false ? 10 : 20) : 30, 20, "Nested ternary in the true branch of outer ternary");
|
||||
assert.strictEqual(false ? (false ? 10 : 20) : 30, 30, "Nested ternary in the false branch of outer ternary");
|
||||
assert.deepEqual(undefined ? undefined : null, null, "Condition is falsy, returns null");
|
||||
assert.strictEqual(null ? undefined : undefined, undefined, "Condition is falsy, returns undefined");
|
||||
});
|
||||
|
||||
describe("conditionalExpression - Origami", async() => {
|
||||
assert.strictEqual(await oriEval("true ? 42 : 0"), 42, "Condition is true, evaluates and returns the first operand");
|
||||
assert.strictEqual(await oriEval("false ? 42 : 0"), 0, "Condition is false, evaluates and returns the second operand");
|
||||
assert.strictEqual(await oriEval("1 ? 'yes' : 'no'"), "yes", "Truthy condition with string operands");
|
||||
assert.strictEqual(await oriEval("0 ? 'yes' : 'no'"), "no", "Falsy condition with string operands");
|
||||
assert.strictEqual(await oriEval("'non-empty' ? 1 : 2"), 1, "Truthy string condition with numeric operands");
|
||||
assert.strictEqual(await oriEval("'' ? 1 : 2"), 2, "Falsy string condition with numeric operands");
|
||||
assert.strictEqual(await oriEval("null ? 'a' : 'b'"), "b", "Falsy null condition");
|
||||
assert.strictEqual(await oriEval("undefined ? 'a' : 'b'"), "b", "Falsy undefined condition");
|
||||
assert.strictEqual(await oriEval("NaN ? 'a' : 'b'"), "b", "Falsy NaN condition");
|
||||
assert.strictEqual(await oriEval("42 ? true : false"), true, "Truthy numeric condition with boolean operands");
|
||||
assert.strictEqual(await oriEval("0 ? true : false"), false, "Falsy numeric condition with boolean operands");
|
||||
assert.strictEqual(await oriEval("[] ? 'array' : 'no array'"), "array", "Truthy array condition");
|
||||
assert.strictEqual(await oriEval("{} ? 'object' : 'no object'"), "object", "Truthy object condition");
|
||||
assert.strictEqual(await oriEval("false ? null : undefined"), undefined, "Condition is false, returns undefined");
|
||||
assert.deepEqual(await oriEval("null ? null : null"), null, "Condition is falsy, returns null");
|
||||
assert.strictEqual(await oriEval("true ? NaN : 42"), NaN, "Condition is true, evaluates and returns NaN");
|
||||
assert.strictEqual(await oriEval("(true ? 1 : 2) ? 3 : 4"), 3, "Nested ternary where first expression evaluates to 1, which is truthy");
|
||||
assert.strictEqual(await oriEval("(false ? 1 : 2) ? 3 : 4"), 3, "Nested ternary where first expression evaluates to 2, which is truthy");
|
||||
assert.strictEqual(await oriEval("(false ? 1 : 0) ? 3 : 4"), 4, "Nested ternary where first expression evaluates to 0, which is falsy");
|
||||
assert.strictEqual(await oriEval("true ? (false ? 10 : 20) : 30"), 20, "Nested ternary in the true branch of outer ternary");
|
||||
assert.strictEqual(await oriEval("false ? (false ? 10 : 20) : 30"), 30, "Nested ternary in the false branch of outer ternary");
|
||||
assert.deepEqual(await oriEval("undefined ? undefined : null"), null, "Condition is falsy, returns null");
|
||||
assert.strictEqual(await oriEval("null ? undefined : undefined"), undefined, "Condition is falsy, returns undefined");
|
||||
});
|
||||
80
node_modules/@weborigami/language/test/generated/logicalAndExpression.test.js
generated
vendored
Normal file
80
node_modules/@weborigami/language/test/generated/logicalAndExpression.test.js
generated
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
// Generated tests -- do not edit directly
|
||||
// @ts-nocheck
|
||||
|
||||
import assert from "node:assert";
|
||||
import { describe } from "node:test";
|
||||
import oriEval from "../generator/oriEval.js";
|
||||
|
||||
describe("logicalAndExpression - JavaScript", () => {
|
||||
assert.strictEqual(true && true, true, "Both operands are true");
|
||||
assert.strictEqual(true && false, false, "First operand is true, second is false");
|
||||
assert.strictEqual(false && true, false, "First operand is false, second is true");
|
||||
assert.strictEqual(false && false, false, "Both operands are false");
|
||||
assert.strictEqual(false && (1 / 0), false, "Short-circuit evaluation: first operand false, second not evaluated");
|
||||
assert.strictEqual(true && 42, 42, "Short-circuit evaluation: first operand true, evaluates second");
|
||||
assert.strictEqual(0 && true, 0, "Short-circuiting with falsy value (0)");
|
||||
assert.strictEqual(true && 'string', "string", "Truthy value with string");
|
||||
assert.strictEqual(false && 'string', false, "Falsy value with string");
|
||||
assert.strictEqual(1 && 0, 0, "Truthy numeric value with falsy numeric value");
|
||||
assert.strictEqual(0 && 1, 0, "Falsy numeric value with truthy numeric value");
|
||||
assert.strictEqual('' && 'non-empty string', "", "Falsy string value with truthy string");
|
||||
assert.strictEqual('non-empty string' && '', "", "Truthy string with falsy string");
|
||||
assert.strictEqual({} && true, true, "Empty object as first operand");
|
||||
assert.deepEqual(true && {}, {}, "Empty object as second operand");
|
||||
assert.strictEqual([] && true, true, "Array as first operand");
|
||||
assert.deepEqual(true && [], [], "Array as second operand");
|
||||
assert.deepEqual(null && true, null, "Null as first operand");
|
||||
assert.deepEqual(true && null, null, "Null as second operand");
|
||||
assert.strictEqual(undefined && true, undefined, "Undefined as first operand");
|
||||
assert.strictEqual(true && undefined, undefined, "Undefined as second operand");
|
||||
assert.strictEqual(NaN && true, NaN, "NaN as first operand");
|
||||
assert.strictEqual(true && NaN, NaN, "NaN as second operand");
|
||||
assert.strictEqual((true && false) && true, false, "Nested logical ANDs with a false in the middle");
|
||||
assert.strictEqual((true && true) && true, true, "Nested logical ANDs with all true");
|
||||
assert.strictEqual(true && (true && false), false, "Nested logical ANDs with false in inner");
|
||||
assert.strictEqual((true && (false && true)), false, "Complex nesting with false at inner-most");
|
||||
assert.strictEqual(true && (3 || 0), 3, "Logical AND with logical OR");
|
||||
assert.strictEqual(true && (0 || 3), 3, "Logical AND with logical OR and falsy values");
|
||||
assert.strictEqual('' && false, "", "Falsy string and false");
|
||||
assert.strictEqual(false && '', false, "False and falsy string");
|
||||
assert.strictEqual(undefined && null, undefined, "Undefined and null");
|
||||
assert.deepEqual(null && undefined, null, "Null and undefined");
|
||||
assert.strictEqual((false && true) && undefined, false, "Short-circuiting nested AND with undefined");
|
||||
});
|
||||
|
||||
describe("logicalAndExpression - Origami", async() => {
|
||||
assert.strictEqual(await oriEval("true && true"), true, "Both operands are true");
|
||||
assert.strictEqual(await oriEval("true && false"), false, "First operand is true, second is false");
|
||||
assert.strictEqual(await oriEval("false && true"), false, "First operand is false, second is true");
|
||||
assert.strictEqual(await oriEval("false && false"), false, "Both operands are false");
|
||||
assert.strictEqual(await oriEval("false && (1 / 0)"), false, "Short-circuit evaluation: first operand false, second not evaluated");
|
||||
assert.strictEqual(await oriEval("true && 42"), 42, "Short-circuit evaluation: first operand true, evaluates second");
|
||||
assert.strictEqual(await oriEval("0 && true"), 0, "Short-circuiting with falsy value (0)");
|
||||
assert.strictEqual(await oriEval("true && 'string'"), "string", "Truthy value with string");
|
||||
assert.strictEqual(await oriEval("false && 'string'"), false, "Falsy value with string");
|
||||
assert.strictEqual(await oriEval("1 && 0"), 0, "Truthy numeric value with falsy numeric value");
|
||||
assert.strictEqual(await oriEval("0 && 1"), 0, "Falsy numeric value with truthy numeric value");
|
||||
assert.strictEqual(await oriEval("'' && 'non-empty string'"), "", "Falsy string value with truthy string");
|
||||
assert.strictEqual(await oriEval("'non-empty string' && ''"), "", "Truthy string with falsy string");
|
||||
assert.strictEqual(await oriEval("{} && true"), true, "Empty object as first operand");
|
||||
assert.deepEqual(await oriEval("true && {}"), {}, "Empty object as second operand");
|
||||
assert.strictEqual(await oriEval("[] && true"), true, "Array as first operand");
|
||||
assert.deepEqual(await oriEval("true && []"), [], "Array as second operand");
|
||||
assert.deepEqual(await oriEval("null && true"), null, "Null as first operand");
|
||||
assert.deepEqual(await oriEval("true && null"), null, "Null as second operand");
|
||||
assert.strictEqual(await oriEval("undefined && true"), undefined, "Undefined as first operand");
|
||||
assert.strictEqual(await oriEval("true && undefined"), undefined, "Undefined as second operand");
|
||||
assert.strictEqual(await oriEval("NaN && true"), NaN, "NaN as first operand");
|
||||
assert.strictEqual(await oriEval("true && NaN"), NaN, "NaN as second operand");
|
||||
assert.strictEqual(await oriEval("(true && false) && true"), false, "Nested logical ANDs with a false in the middle");
|
||||
assert.strictEqual(await oriEval("(true && true) && true"), true, "Nested logical ANDs with all true");
|
||||
assert.strictEqual(await oriEval("true && (true && false)"), false, "Nested logical ANDs with false in inner");
|
||||
assert.strictEqual(await oriEval("(true && (false && true))"), false, "Complex nesting with false at inner-most");
|
||||
assert.strictEqual(await oriEval("true && (3 || 0)"), 3, "Logical AND with logical OR");
|
||||
assert.strictEqual(await oriEval("true && (0 || 3)"), 3, "Logical AND with logical OR and falsy values");
|
||||
assert.strictEqual(await oriEval("'' && false"), "", "Falsy string and false");
|
||||
assert.strictEqual(await oriEval("false && ''"), false, "False and falsy string");
|
||||
assert.strictEqual(await oriEval("undefined && null"), undefined, "Undefined and null");
|
||||
assert.deepEqual(await oriEval("null && undefined"), null, "Null and undefined");
|
||||
assert.strictEqual(await oriEval("(false && true) && undefined"), false, "Short-circuiting nested AND with undefined");
|
||||
});
|
||||
78
node_modules/@weborigami/language/test/generated/logicalOrExpression.test.js
generated
vendored
Normal file
78
node_modules/@weborigami/language/test/generated/logicalOrExpression.test.js
generated
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
// Generated tests -- do not edit directly
|
||||
// @ts-nocheck
|
||||
|
||||
import assert from "node:assert";
|
||||
import { describe } from "node:test";
|
||||
import oriEval from "../generator/oriEval.js";
|
||||
|
||||
describe("logicalOrExpression - JavaScript", () => {
|
||||
assert.strictEqual(true || true, true, "Both operands are true");
|
||||
assert.strictEqual(true || false, true, "First operand is true, second is false");
|
||||
assert.strictEqual(false || true, true, "First operand is false, second is true");
|
||||
assert.strictEqual(false || false, false, "Both operands are false");
|
||||
assert.strictEqual(false || 42, 42, "Short-circuit evaluation: first operand false, evaluates second");
|
||||
assert.strictEqual(0 || true, true, "Falsy value (0) with truthy second operand");
|
||||
assert.strictEqual(true || 'string', true, "Truthy first operand, string second operand not evaluated");
|
||||
assert.strictEqual(false || 'string', "string", "Falsy first operand, evaluates string second operand");
|
||||
assert.strictEqual(1 || 0, 1, "Truthy numeric value with falsy numeric value");
|
||||
assert.strictEqual(0 || 1, 1, "Falsy numeric value with truthy numeric value");
|
||||
assert.strictEqual('' || 'non-empty string', "non-empty string", "Falsy string value with truthy string");
|
||||
assert.strictEqual('non-empty string' || '', "non-empty string", "Truthy string with falsy string");
|
||||
assert.deepEqual({} || true, {}, "Empty object as first operand");
|
||||
assert.strictEqual(true || {}, true, "True as first operand, object not evaluated");
|
||||
assert.deepEqual([] || true, [], "Array as first operand");
|
||||
assert.strictEqual(true || [], true, "True as first operand, array not evaluated");
|
||||
assert.strictEqual(null || true, true, "Null as first operand");
|
||||
assert.strictEqual(true || null, true, "True as first operand, null not evaluated");
|
||||
assert.strictEqual(undefined || true, true, "Undefined as first operand");
|
||||
assert.strictEqual(true || undefined, true, "True as first operand, undefined not evaluated");
|
||||
assert.strictEqual(NaN || true, true, "NaN as first operand");
|
||||
assert.strictEqual(true || NaN, true, "True as first operand, NaN not evaluated");
|
||||
assert.strictEqual((false || true) || false, true, "Nested logical ORs with a true in the middle");
|
||||
assert.strictEqual((false || false) || true, true, "Nested logical ORs with a true at the end");
|
||||
assert.strictEqual(false || (false || true), true, "Nested logical ORs with true in inner");
|
||||
assert.strictEqual((false || (true || false)), true, "Complex nesting with true at inner-most");
|
||||
assert.strictEqual(false || (3 && 0), 0, "Logical OR with logical AND and falsy result");
|
||||
assert.strictEqual(false || (0 && 3), 0, "Logical OR with logical AND and falsy first operand");
|
||||
assert.strictEqual('' || false, false, "Falsy string and false");
|
||||
assert.strictEqual(false || '', "", "False and falsy string");
|
||||
assert.deepEqual(undefined || null, null, "Undefined and null");
|
||||
assert.strictEqual(null || undefined, undefined, "Null and undefined");
|
||||
assert.strictEqual((true || false) || undefined, true, "Short-circuiting nested OR with undefined");
|
||||
});
|
||||
|
||||
describe("logicalOrExpression - Origami", async() => {
|
||||
assert.strictEqual(await oriEval("true || true"), true, "Both operands are true");
|
||||
assert.strictEqual(await oriEval("true || false"), true, "First operand is true, second is false");
|
||||
assert.strictEqual(await oriEval("false || true"), true, "First operand is false, second is true");
|
||||
assert.strictEqual(await oriEval("false || false"), false, "Both operands are false");
|
||||
assert.strictEqual(await oriEval("false || 42"), 42, "Short-circuit evaluation: first operand false, evaluates second");
|
||||
assert.strictEqual(await oriEval("0 || true"), true, "Falsy value (0) with truthy second operand");
|
||||
assert.strictEqual(await oriEval("true || 'string'"), true, "Truthy first operand, string second operand not evaluated");
|
||||
assert.strictEqual(await oriEval("false || 'string'"), "string", "Falsy first operand, evaluates string second operand");
|
||||
assert.strictEqual(await oriEval("1 || 0"), 1, "Truthy numeric value with falsy numeric value");
|
||||
assert.strictEqual(await oriEval("0 || 1"), 1, "Falsy numeric value with truthy numeric value");
|
||||
assert.strictEqual(await oriEval("'' || 'non-empty string'"), "non-empty string", "Falsy string value with truthy string");
|
||||
assert.strictEqual(await oriEval("'non-empty string' || ''"), "non-empty string", "Truthy string with falsy string");
|
||||
assert.deepEqual(await oriEval("{} || true"), {}, "Empty object as first operand");
|
||||
assert.strictEqual(await oriEval("true || {}"), true, "True as first operand, object not evaluated");
|
||||
assert.deepEqual(await oriEval("[] || true"), [], "Array as first operand");
|
||||
assert.strictEqual(await oriEval("true || []"), true, "True as first operand, array not evaluated");
|
||||
assert.strictEqual(await oriEval("null || true"), true, "Null as first operand");
|
||||
assert.strictEqual(await oriEval("true || null"), true, "True as first operand, null not evaluated");
|
||||
assert.strictEqual(await oriEval("undefined || true"), true, "Undefined as first operand");
|
||||
assert.strictEqual(await oriEval("true || undefined"), true, "True as first operand, undefined not evaluated");
|
||||
assert.strictEqual(await oriEval("NaN || true"), true, "NaN as first operand");
|
||||
assert.strictEqual(await oriEval("true || NaN"), true, "True as first operand, NaN not evaluated");
|
||||
assert.strictEqual(await oriEval("(false || true) || false"), true, "Nested logical ORs with a true in the middle");
|
||||
assert.strictEqual(await oriEval("(false || false) || true"), true, "Nested logical ORs with a true at the end");
|
||||
assert.strictEqual(await oriEval("false || (false || true)"), true, "Nested logical ORs with true in inner");
|
||||
assert.strictEqual(await oriEval("(false || (true || false))"), true, "Complex nesting with true at inner-most");
|
||||
assert.strictEqual(await oriEval("false || (3 && 0)"), 0, "Logical OR with logical AND and falsy result");
|
||||
assert.strictEqual(await oriEval("false || (0 && 3)"), 0, "Logical OR with logical AND and falsy first operand");
|
||||
assert.strictEqual(await oriEval("'' || false"), false, "Falsy string and false");
|
||||
assert.strictEqual(await oriEval("false || ''"), "", "False and falsy string");
|
||||
assert.deepEqual(await oriEval("undefined || null"), null, "Undefined and null");
|
||||
assert.strictEqual(await oriEval("null || undefined"), undefined, "Null and undefined");
|
||||
assert.strictEqual(await oriEval("(true || false) || undefined"), true, "Short-circuiting nested OR with undefined");
|
||||
});
|
||||
64
node_modules/@weborigami/language/test/generated/nullishCoalescingExpression.test.js
generated
vendored
Normal file
64
node_modules/@weborigami/language/test/generated/nullishCoalescingExpression.test.js
generated
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
// Generated tests -- do not edit directly
|
||||
// @ts-nocheck
|
||||
|
||||
import assert from "node:assert";
|
||||
import { describe } from "node:test";
|
||||
import oriEval from "../generator/oriEval.js";
|
||||
|
||||
describe("nullishCoalescingExpression - JavaScript", () => {
|
||||
assert.strictEqual(null ?? 42, 42, "Left operand is null, returns right operand");
|
||||
assert.strictEqual(undefined ?? 42, 42, "Left operand is undefined, returns right operand");
|
||||
assert.strictEqual(0 ?? 42, 0, "Left operand is 0 (falsy but not nullish), returns left operand");
|
||||
assert.strictEqual('' ?? 'default', "", "Left operand is an empty string (falsy but not nullish), returns left operand");
|
||||
assert.strictEqual(false ?? true, false, "Left operand is false (falsy but not nullish), returns left operand");
|
||||
assert.strictEqual(42 ?? 0, 42, "Left operand is a non-nullish truthy value, returns left operand");
|
||||
assert.strictEqual(null ?? undefined, undefined, "Left operand is null, returns right operand which is undefined");
|
||||
assert.deepEqual(undefined ?? null, null, "Left operand is undefined, returns right operand which is null");
|
||||
assert.strictEqual(NaN ?? 42, NaN, "Left operand is NaN (not nullish), returns left operand");
|
||||
assert.deepEqual([] ?? 'default', [], "Left operand is an empty array (not nullish), returns left operand");
|
||||
assert.deepEqual({} ?? 'default', {}, "Left operand is an empty object (not nullish), returns left operand");
|
||||
assert.strictEqual((null ?? 42) ?? 50, 42, "Nested nullish coalescing, first nullish operand replaced, second ignored");
|
||||
assert.strictEqual((undefined ?? null) ?? 'fallback', "fallback", "Nested nullish coalescing");
|
||||
assert.strictEqual((0 ?? null) ?? 'fallback', 0, "Nested nullish coalescing with falsy but non-nullish value");
|
||||
assert.strictEqual(null ?? (undefined ?? 42), 42, "Nullish coalescing in the right operand");
|
||||
assert.strictEqual(null ?? null ?? null ?? 'fallback', "fallback", "Chained nullish coalescing, resolves to the last non-nullish value");
|
||||
assert.strictEqual(null ?? (false ?? 'default'), false, "Right operand evaluates to non-nullish falsy value");
|
||||
assert.strictEqual(null ?? (true ?? 'default'), true, "Right operand evaluates to truthy value");
|
||||
assert.strictEqual(42 ?? (null ?? 0), 42, "Left operand is not nullish, ignores right operand");
|
||||
assert.strictEqual(undefined ?? null ?? 'value', "value", "Chained nullish coalescing with undefined and null");
|
||||
assert.strictEqual((NaN ?? null) ?? 42, NaN, "Left operand is NaN, not nullish, returns NaN");
|
||||
assert.strictEqual((undefined ?? NaN) ?? 42, NaN, "Right operand resolves to NaN");
|
||||
assert.strictEqual(null ?? 'default' ?? 42, "default", "Chained nullish coalescing, resolves to first non-nullish value");
|
||||
assert.strictEqual('' ?? 'default' ?? 42, "", "Falsy but non-nullish value, returns left operand");
|
||||
assert.strictEqual(null ?? undefined ?? NaN, NaN, "Chained nullish coalescing, resolves to NaN as the first non-nullish value");
|
||||
assert.strictEqual((null ?? null) ?? undefined, undefined, "Nested nullish coalescing resolves to undefined");
|
||||
});
|
||||
|
||||
describe("nullishCoalescingExpression - Origami", async() => {
|
||||
assert.strictEqual(await oriEval("null ?? 42"), 42, "Left operand is null, returns right operand");
|
||||
assert.strictEqual(await oriEval("undefined ?? 42"), 42, "Left operand is undefined, returns right operand");
|
||||
assert.strictEqual(await oriEval("0 ?? 42"), 0, "Left operand is 0 (falsy but not nullish), returns left operand");
|
||||
assert.strictEqual(await oriEval("'' ?? 'default'"), "", "Left operand is an empty string (falsy but not nullish), returns left operand");
|
||||
assert.strictEqual(await oriEval("false ?? true"), false, "Left operand is false (falsy but not nullish), returns left operand");
|
||||
assert.strictEqual(await oriEval("42 ?? 0"), 42, "Left operand is a non-nullish truthy value, returns left operand");
|
||||
assert.strictEqual(await oriEval("null ?? undefined"), undefined, "Left operand is null, returns right operand which is undefined");
|
||||
assert.deepEqual(await oriEval("undefined ?? null"), null, "Left operand is undefined, returns right operand which is null");
|
||||
assert.strictEqual(await oriEval("NaN ?? 42"), NaN, "Left operand is NaN (not nullish), returns left operand");
|
||||
assert.deepEqual(await oriEval("[] ?? 'default'"), [], "Left operand is an empty array (not nullish), returns left operand");
|
||||
assert.deepEqual(await oriEval("{} ?? 'default'"), {}, "Left operand is an empty object (not nullish), returns left operand");
|
||||
assert.strictEqual(await oriEval("(null ?? 42) ?? 50"), 42, "Nested nullish coalescing, first nullish operand replaced, second ignored");
|
||||
assert.strictEqual(await oriEval("(undefined ?? null) ?? 'fallback'"), "fallback", "Nested nullish coalescing");
|
||||
assert.strictEqual(await oriEval("(0 ?? null) ?? 'fallback'"), 0, "Nested nullish coalescing with falsy but non-nullish value");
|
||||
assert.strictEqual(await oriEval("null ?? (undefined ?? 42)"), 42, "Nullish coalescing in the right operand");
|
||||
assert.strictEqual(await oriEval("null ?? null ?? null ?? 'fallback'"), "fallback", "Chained nullish coalescing, resolves to the last non-nullish value");
|
||||
assert.strictEqual(await oriEval("null ?? (false ?? 'default')"), false, "Right operand evaluates to non-nullish falsy value");
|
||||
assert.strictEqual(await oriEval("null ?? (true ?? 'default')"), true, "Right operand evaluates to truthy value");
|
||||
assert.strictEqual(await oriEval("42 ?? (null ?? 0)"), 42, "Left operand is not nullish, ignores right operand");
|
||||
assert.strictEqual(await oriEval("undefined ?? null ?? 'value'"), "value", "Chained nullish coalescing with undefined and null");
|
||||
assert.strictEqual(await oriEval("(NaN ?? null) ?? 42"), NaN, "Left operand is NaN, not nullish, returns NaN");
|
||||
assert.strictEqual(await oriEval("(undefined ?? NaN) ?? 42"), NaN, "Right operand resolves to NaN");
|
||||
assert.strictEqual(await oriEval("null ?? 'default' ?? 42"), "default", "Chained nullish coalescing, resolves to first non-nullish value");
|
||||
assert.strictEqual(await oriEval("'' ?? 'default' ?? 42"), "", "Falsy but non-nullish value, returns left operand");
|
||||
assert.strictEqual(await oriEval("null ?? undefined ?? NaN"), NaN, "Chained nullish coalescing, resolves to NaN as the first non-nullish value");
|
||||
assert.strictEqual(await oriEval("(null ?? null) ?? undefined"), undefined, "Nested nullish coalescing resolves to undefined");
|
||||
});
|
||||
80
node_modules/@weborigami/language/test/generator/generateTests.js
generated
vendored
Normal file
80
node_modules/@weborigami/language/test/generator/generateTests.js
generated
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
// Validate that the tests produce the expected results in JavaScript itself.
|
||||
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
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;
|
||||
|
||||
export default async function generateTests(inputDirectory, outputDirectory) {
|
||||
const filenames = await fs.readdir(inputDirectory);
|
||||
const yamlFilenames = filenames.filter((filename) =>
|
||||
filename.endsWith(".yaml")
|
||||
);
|
||||
for (const yamlFilename of yamlFilenames) {
|
||||
const basename = path.basename(yamlFilename, ".yaml");
|
||||
|
||||
const casesPath = path.join(inputDirectory, yamlFilename);
|
||||
const text = String(await fs.readFile(casesPath));
|
||||
const cases = YAML.parse(text);
|
||||
const transformed = cases.map(transformCase);
|
||||
const result = tests(basename, transformed);
|
||||
|
||||
const outputName = basename + ".test.js";
|
||||
const outputPath = path.join(outputDirectory, outputName);
|
||||
await fs.writeFile(outputPath, result);
|
||||
}
|
||||
}
|
||||
|
||||
function javaScriptTest({ assertType, source, expectedJs, description }) {
|
||||
return ` assert.${assertType}(${source}, ${expectedJs}, "${description}");`;
|
||||
}
|
||||
|
||||
function origamiTest({ assertType, source, expectedJs, description }) {
|
||||
return ` assert.${assertType}(await oriEval("${source}"), ${expectedJs}, "${description}");`;
|
||||
}
|
||||
|
||||
function tests(suiteName, cases) {
|
||||
return `// Generated tests -- do not edit directly
|
||||
// @ts-nocheck
|
||||
|
||||
import assert from "node:assert";
|
||||
import { describe } from "node:test";
|
||||
import oriEval from "../generator/oriEval.js";
|
||||
|
||||
describe("${suiteName} - JavaScript", () => {
|
||||
${cases.map(javaScriptTest).join("\n")}
|
||||
});
|
||||
|
||||
describe("${suiteName} - Origami", async() => {
|
||||
${cases.map(origamiTest).join("\n")}
|
||||
});`;
|
||||
}
|
||||
// Transform parsed YAML values into values suitable for testing
|
||||
function transformCase({ description, expected, source }) {
|
||||
const markers = {
|
||||
__null__: null,
|
||||
__undefined__: undefined,
|
||||
__NaN__: NaN,
|
||||
};
|
||||
if (expected in markers) {
|
||||
expected = markers[expected];
|
||||
}
|
||||
const assertType = typeof expected === "object" ? "deepEqual" : "strictEqual";
|
||||
const expectedJs =
|
||||
typeof expected === "string"
|
||||
? `"${expected}"`
|
||||
: typeof expected === "object" && expected !== null
|
||||
? JSON.stringify(expected)
|
||||
: expected;
|
||||
return { assertType, description, expected, expectedJs, source };
|
||||
}
|
||||
|
||||
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const casesDirectory = path.join(dirname, "../cases");
|
||||
const generatedDirectory = path.join(dirname, "../generated");
|
||||
await generateTests(casesDirectory, generatedDirectory);
|
||||
15
node_modules/@weborigami/language/test/generator/oriEval.js
generated
vendored
Normal file
15
node_modules/@weborigami/language/test/generator/oriEval.js
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ObjectTree } from "@weborigami/async-tree";
|
||||
import * as compile from "../../src/compiler/compile.js";
|
||||
|
||||
export default async function oriEval(source) {
|
||||
const builtins = new ObjectTree({
|
||||
false: false,
|
||||
NaN: NaN,
|
||||
null: null,
|
||||
true: true,
|
||||
undefined: undefined,
|
||||
});
|
||||
const compiled = compile.program(source);
|
||||
const result = await compiled.call(builtins);
|
||||
return result;
|
||||
}
|
||||
68
node_modules/@weborigami/language/test/runtime/EventTargetMixin.test.js
generated
vendored
Normal file
68
node_modules/@weborigami/language/test/runtime/EventTargetMixin.test.js
generated
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
import assert from "node:assert";
|
||||
import { describe, test } from "node:test";
|
||||
import EventTargetMixin from "../../src/runtime/EventTargetMixin.js";
|
||||
|
||||
class EventTargetTest extends EventTargetMixin(Object) {}
|
||||
|
||||
describe("EventTargetMixin", () => {
|
||||
test("add and dispatch event", () => {
|
||||
const fixture = new EventTargetTest();
|
||||
const event = new Event("test");
|
||||
let callCount = 0;
|
||||
const callback = () => {
|
||||
callCount++;
|
||||
};
|
||||
fixture.addEventListener("test", callback);
|
||||
// Add twice, ensure that the callback is only called once.
|
||||
fixture.addEventListener("test", callback);
|
||||
const dispatched = fixture.dispatchEvent(event);
|
||||
assert(dispatched);
|
||||
assert.equal(callCount, 1);
|
||||
});
|
||||
|
||||
test("dispatch event with no listeners", () => {
|
||||
const fixture = new EventTargetTest();
|
||||
const event = new Event("test");
|
||||
const takeDefaultAction = fixture.dispatchEvent(event);
|
||||
assert(takeDefaultAction);
|
||||
});
|
||||
|
||||
test("remove event listener", () => {
|
||||
const fixture = new EventTargetTest();
|
||||
const event = new Event("test");
|
||||
let callCount = 0;
|
||||
const callback = () => {
|
||||
callCount++;
|
||||
};
|
||||
fixture.addEventListener("test", callback);
|
||||
fixture.removeEventListener("test", callback);
|
||||
fixture.dispatchEvent(event);
|
||||
assert.equal(callCount, 0);
|
||||
});
|
||||
|
||||
test("stop immediate propagation", () => {
|
||||
const fixture = new EventTargetTest();
|
||||
const event = new Event("test");
|
||||
let callCount = 0;
|
||||
fixture.addEventListener("test", (event) => {
|
||||
callCount++;
|
||||
event.stopImmediatePropagation();
|
||||
});
|
||||
fixture.addEventListener("test", () => {
|
||||
callCount++;
|
||||
});
|
||||
fixture.dispatchEvent(event);
|
||||
assert.equal(callCount, 1);
|
||||
});
|
||||
|
||||
test("prevent default", () => {
|
||||
const fixture = new EventTargetTest();
|
||||
const event = new Event("test");
|
||||
fixture.addEventListener("test", (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
const takeDefaultAction = fixture.dispatchEvent(event);
|
||||
assert(!takeDefaultAction);
|
||||
assert(event.defaultPrevented);
|
||||
});
|
||||
});
|
||||
37
node_modules/@weborigami/language/test/runtime/OrigamiFiles.test.js
generated
vendored
Normal file
37
node_modules/@weborigami/language/test/runtime/OrigamiFiles.test.js
generated
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
import assert from "node:assert";
|
||||
import * as fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, test } from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import OrigamiFiles from "../../src/runtime/OrigamiFiles.js";
|
||||
|
||||
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const tempDirectory = path.join(dirname, "fixtures/temp");
|
||||
|
||||
describe("OrigamiFiles", () => {
|
||||
test("can watch its folder for changes", { timeout: 2000 }, async () => {
|
||||
await createTempDirectory();
|
||||
const tempFiles = new OrigamiFiles(tempDirectory);
|
||||
const changedFileName = await new Promise(async (resolve) => {
|
||||
// @ts-ignore
|
||||
tempFiles.addEventListener("change", (event) => {
|
||||
resolve(/** @type {any} */ (event).options.key);
|
||||
});
|
||||
// @ts-ignore
|
||||
await tempFiles.set(
|
||||
"foo.txt",
|
||||
"This file is left over from testing and can be removed."
|
||||
);
|
||||
});
|
||||
await removeTempDirectory();
|
||||
assert.equal(changedFileName, "foo.txt");
|
||||
});
|
||||
});
|
||||
|
||||
async function createTempDirectory() {
|
||||
await fs.mkdir(tempDirectory, { recursive: true });
|
||||
}
|
||||
|
||||
async function removeTempDirectory() {
|
||||
await fs.rm(tempDirectory, { recursive: true });
|
||||
}
|
||||
85
node_modules/@weborigami/language/test/runtime/evaluate.test.js
generated
vendored
Normal file
85
node_modules/@weborigami/language/test/runtime/evaluate.test.js
generated
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
import { ObjectTree } from "@weborigami/async-tree";
|
||||
import assert from "node:assert";
|
||||
import { describe, test } from "node:test";
|
||||
import * as ops from "../../src/runtime/ops.js";
|
||||
|
||||
import evaluate from "../../src/runtime/evaluate.js";
|
||||
|
||||
describe("evaluate", () => {
|
||||
test("can retrieve values from scope", async () => {
|
||||
const code = createCode([ops.scope, "message"]);
|
||||
const parent = new ObjectTree({
|
||||
message: "Hello",
|
||||
});
|
||||
const tree = new ObjectTree({});
|
||||
tree.parent = parent;
|
||||
const result = await evaluate.call(tree, code);
|
||||
assert.equal(result, "Hello");
|
||||
});
|
||||
|
||||
test("can invoke functions in scope", async () => {
|
||||
// Match the array representation of code generated by the parser.
|
||||
const code = createCode([
|
||||
[ops.scope, "greet"],
|
||||
[ops.scope, "name"],
|
||||
]);
|
||||
|
||||
const tree = new ObjectTree({
|
||||
async greet(name) {
|
||||
return `Hello ${name}`;
|
||||
},
|
||||
name: "world",
|
||||
});
|
||||
|
||||
const result = await evaluate.call(tree, code);
|
||||
assert.equal(result, "Hello world");
|
||||
});
|
||||
|
||||
test("passes context to invoked functions", async () => {
|
||||
const code = createCode([ops.scope, "fn"]);
|
||||
const tree = new ObjectTree({
|
||||
async fn() {
|
||||
assert.equal(this, tree);
|
||||
},
|
||||
});
|
||||
await evaluate.call(tree, code);
|
||||
});
|
||||
|
||||
test("evaluates a function with fixed number of arguments", async () => {
|
||||
const fn = (x, y) => ({
|
||||
c: `${x}${y}c`,
|
||||
});
|
||||
const code = createCode([ops.traverse, fn, "a", "b", "c"]);
|
||||
assert.equal(await evaluate.call(null, code), "abc");
|
||||
});
|
||||
|
||||
test("if object in function position isn't a function, can unpack it", async () => {
|
||||
const fn = (...args) => args.join(",");
|
||||
const packed = new String();
|
||||
/** @type {any} */ (packed).unpack = async () => fn;
|
||||
const code = createCode([packed, "a", "b", "c"]);
|
||||
const result = await evaluate.call(null, code);
|
||||
assert.equal(result, "a,b,c");
|
||||
});
|
||||
|
||||
test("by defalut sets the parent of a returned tree to the current tree", async () => {
|
||||
const fn = () => new ObjectTree({});
|
||||
const code = createCode([fn]);
|
||||
const tree = new ObjectTree({});
|
||||
const result = await evaluate.call(tree, code);
|
||||
assert.equal(result.parent, tree);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @returns {import("../../index.ts").Code}
|
||||
*/
|
||||
function createCode(array) {
|
||||
const code = array;
|
||||
/** @type {any} */ (code).location = {
|
||||
source: {
|
||||
text: "",
|
||||
},
|
||||
};
|
||||
return code;
|
||||
}
|
||||
76
node_modules/@weborigami/language/test/runtime/expressionObject.test.js
generated
vendored
Normal file
76
node_modules/@weborigami/language/test/runtime/expressionObject.test.js
generated
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
import { ObjectTree, symbols, Tree } from "@weborigami/async-tree";
|
||||
import assert from "node:assert";
|
||||
import { describe, test } from "node:test";
|
||||
|
||||
import expressionObject from "../../src/runtime/expressionObject.js";
|
||||
import { ops } from "../../src/runtime/internal.js";
|
||||
|
||||
describe("expressionObject", () => {
|
||||
test("can instantiate an object", async () => {
|
||||
const scope = new ObjectTree({
|
||||
upper: (s) => s.toUpperCase(),
|
||||
});
|
||||
|
||||
const entries = [
|
||||
["hello", [[ops.scope, "upper"], "hello"]],
|
||||
["world", [[ops.scope, "upper"], "world"]],
|
||||
];
|
||||
|
||||
const object = await expressionObject(entries, scope);
|
||||
assert.equal(await object.hello, "HELLO");
|
||||
assert.equal(await object.world, "WORLD");
|
||||
assert.equal(object[symbols.parent], scope);
|
||||
});
|
||||
|
||||
test("can define a property getter", async () => {
|
||||
let count = 0;
|
||||
const increment = () => count++;
|
||||
const entries = [["count", [ops.getter, [increment]]]];
|
||||
const object = await expressionObject(entries, null);
|
||||
assert.equal(await object.count, 0);
|
||||
assert.equal(await object.count, 1);
|
||||
});
|
||||
|
||||
test("treats a getter for a primitive value as a regular property", async () => {
|
||||
const entries = [["name", [ops.getter, "world"]]];
|
||||
const object = await expressionObject(entries, null);
|
||||
assert.equal(object.name, "world");
|
||||
});
|
||||
|
||||
test("can instantiate an Origami tree", async () => {
|
||||
const entries = [
|
||||
["name", "world"],
|
||||
["message", [ops.concat, "Hello, ", [ops.scope, "name"], "!"]],
|
||||
];
|
||||
const parent = new ObjectTree({});
|
||||
const object = await expressionObject(entries, parent);
|
||||
assert.deepEqual(await Tree.plain(object), {
|
||||
name: "world",
|
||||
message: "Hello, world!",
|
||||
});
|
||||
assert.equal(object[symbols.parent], parent);
|
||||
});
|
||||
|
||||
test("returned object values can be unpacked", async () => {
|
||||
const entries = [["data.json", `{ "a": 1 }`]];
|
||||
const parent = new ObjectTree({
|
||||
"json.handler": {
|
||||
unpack: JSON.parse,
|
||||
},
|
||||
});
|
||||
const result = await expressionObject(entries, parent);
|
||||
const dataJson = await result["data.json"];
|
||||
const json = await dataJson.unpack();
|
||||
assert.deepEqual(json, { a: 1 });
|
||||
});
|
||||
|
||||
test("a key declared with parentheses is not enumerable", async () => {
|
||||
const entries = [
|
||||
["(hidden)", "shh"],
|
||||
["visible", "hey"],
|
||||
];
|
||||
const object = await expressionObject(entries, null);
|
||||
assert.deepEqual(Object.keys(object), ["visible"]);
|
||||
assert.equal(object["hidden"], "shh");
|
||||
});
|
||||
});
|
||||
1
node_modules/@weborigami/language/test/runtime/fixtures/foo.js
generated
vendored
Normal file
1
node_modules/@weborigami/language/test/runtime/fixtures/foo.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export default () => "bar";
|
||||
1
node_modules/@weborigami/language/test/runtime/fixtures/makeTest/a
generated
vendored
Normal file
1
node_modules/@weborigami/language/test/runtime/fixtures/makeTest/a
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Hello, world.
|
||||
0
node_modules/@weborigami/language/test/runtime/fixtures/makeTest/b = a
generated
vendored
Normal file
0
node_modules/@weborigami/language/test/runtime/fixtures/makeTest/b = a
generated
vendored
Normal file
1
node_modules/@weborigami/language/test/runtime/fixtures/metagraphs/foo.txt
generated
vendored
Normal file
1
node_modules/@weborigami/language/test/runtime/fixtures/metagraphs/foo.txt
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Foo
|
||||
3
node_modules/@weborigami/language/test/runtime/fixtures/metagraphs/greeting = this('world').js
generated
vendored
Normal file
3
node_modules/@weborigami/language/test/runtime/fixtures/metagraphs/greeting = this('world').js
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function (name) {
|
||||
return `Hello, ${name}.`;
|
||||
}
|
||||
5
node_modules/@weborigami/language/test/runtime/fixtures/metagraphs/obj = this.json
generated
vendored
Normal file
5
node_modules/@weborigami/language/test/runtime/fixtures/metagraphs/obj = this.json
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"a": "Hello, a.",
|
||||
"b": "Hello, b.",
|
||||
"c": "Hello, c."
|
||||
}
|
||||
3
node_modules/@weborigami/language/test/runtime/fixtures/metagraphs/sample.txt = this().js
generated
vendored
Normal file
3
node_modules/@weborigami/language/test/runtime/fixtures/metagraphs/sample.txt = this().js
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function () {
|
||||
return "Hello, world.";
|
||||
}
|
||||
1
node_modules/@weborigami/language/test/runtime/fixtures/metagraphs/string = this.json
generated
vendored
Normal file
1
node_modules/@weborigami/language/test/runtime/fixtures/metagraphs/string = this.json
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
"Hello, world."
|
||||
0
node_modules/@weborigami/language/test/runtime/fixtures/metagraphs/value = fn()
generated
vendored
Normal file
0
node_modules/@weborigami/language/test/runtime/fixtures/metagraphs/value = fn()
generated
vendored
Normal file
5
node_modules/@weborigami/language/test/runtime/fixtures/subgraph = this.js
generated
vendored
Normal file
5
node_modules/@weborigami/language/test/runtime/fixtures/subgraph = this.js
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ObjectTree } from "@weborigami/async-tree";
|
||||
export default new ObjectTree({
|
||||
a: "Hello, a.",
|
||||
b: "Hello, b.",
|
||||
});
|
||||
4
node_modules/@weborigami/language/test/runtime/fixtures/templates/greet.orit
generated
vendored
Normal file
4
node_modules/@weborigami/language/test/runtime/fixtures/templates/greet.orit
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
Greetings:
|
||||
|
||||
{{ map(=`Hello, {{ _ }}.
|
||||
`)(names.yaml) }}
|
||||
15
node_modules/@weborigami/language/test/runtime/fixtures/templates/index.orit
generated
vendored
Normal file
15
node_modules/@weborigami/language/test/runtime/fixtures/templates/index.orit
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: Greetings
|
||||
message: !ori title
|
||||
---
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>{{ title }}</title>
|
||||
</head>
|
||||
<body>
|
||||
{{ message }}
|
||||
</body>
|
||||
</html>
|
||||
3
node_modules/@weborigami/language/test/runtime/fixtures/templates/names.yaml
generated
vendored
Normal file
3
node_modules/@weborigami/language/test/runtime/fixtures/templates/names.yaml
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
- Alice
|
||||
- Bob
|
||||
- Carol
|
||||
1
node_modules/@weborigami/language/test/runtime/fixtures/templates/plain.txt
generated
vendored
Normal file
1
node_modules/@weborigami/language/test/runtime/fixtures/templates/plain.txt
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Hello, world.
|
||||
1
node_modules/@weborigami/language/test/runtime/fixtures/virtualKeys/.keys.json
generated
vendored
Normal file
1
node_modules/@weborigami/language/test/runtime/fixtures/virtualKeys/.keys.json
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
["a", "b", "c"]
|
||||
24
node_modules/@weborigami/language/test/runtime/functionResultsMap.test.js
generated
vendored
Normal file
24
node_modules/@weborigami/language/test/runtime/functionResultsMap.test.js
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ObjectTree, Tree, scope } from "@weborigami/async-tree";
|
||||
import assert from "node:assert";
|
||||
import { describe, test } from "node:test";
|
||||
import functionResultsMap from "../../src/runtime/functionResultsMap.js";
|
||||
|
||||
describe("functionResultsMap", () => {
|
||||
test("get() invokes functions using scope, returns other values as is", async () => {
|
||||
const parent = new ObjectTree({
|
||||
message: "Hello",
|
||||
});
|
||||
const tree = new ObjectTree({
|
||||
fn: /** @this {import("@weborigami/types").AsyncTree} */ function () {
|
||||
return scope(this).get("message");
|
||||
},
|
||||
string: "string",
|
||||
});
|
||||
tree.parent = parent;
|
||||
const fixture = functionResultsMap(tree);
|
||||
assert.deepEqual(await Tree.plain(fixture), {
|
||||
fn: "Hello",
|
||||
string: "string",
|
||||
});
|
||||
});
|
||||
});
|
||||
39
node_modules/@weborigami/language/test/runtime/handlers.test.js
generated
vendored
Normal file
39
node_modules/@weborigami/language/test/runtime/handlers.test.js
generated
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
import { ObjectTree } from "@weborigami/async-tree";
|
||||
import assert from "node:assert";
|
||||
import { describe, test } from "node:test";
|
||||
import { handleExtension } from "../../src/runtime/handlers.js";
|
||||
|
||||
describe("handlers", () => {
|
||||
test("attaches an unpack method to a value with an extension", async () => {
|
||||
const fixture = createFixture();
|
||||
const numberValue = await fixture.get("foo");
|
||||
assert(typeof numberValue === "number");
|
||||
assert.equal(numberValue, 1);
|
||||
const jsonFile = await fixture.get("bar.json");
|
||||
const withHandler = await handleExtension(fixture, jsonFile, "bar.json");
|
||||
assert.equal(String(withHandler), `{ "bar": 2 }`);
|
||||
const data = await withHandler.unpack();
|
||||
assert.deepEqual(data, { bar: 2 });
|
||||
});
|
||||
|
||||
test("immediately unpacks if key ends in slash", async () => {
|
||||
const fixture = createFixture();
|
||||
const jsonFile = await fixture.get("bar.json");
|
||||
const data = await handleExtension(fixture, jsonFile, "bar.json/");
|
||||
assert.deepEqual(data, { bar: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
function createFixture() {
|
||||
const parent = new ObjectTree({
|
||||
"json.handler": {
|
||||
unpack: (buffer) => JSON.parse(String(buffer)),
|
||||
},
|
||||
});
|
||||
let tree = new ObjectTree({
|
||||
foo: 1, // No extension, should be left alone
|
||||
"bar.json": `{ "bar": 2 }`,
|
||||
});
|
||||
tree.parent = parent;
|
||||
return tree;
|
||||
}
|
||||
50
node_modules/@weborigami/language/test/runtime/mergeTrees.test.js
generated
vendored
Normal file
50
node_modules/@weborigami/language/test/runtime/mergeTrees.test.js
generated
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Tree } from "@weborigami/async-tree";
|
||||
import assert from "node:assert";
|
||||
import { describe, test } from "node:test";
|
||||
import mergeTrees from "../../src/runtime/mergeTrees.js";
|
||||
|
||||
describe("mergeTrees", () => {
|
||||
test("merges trees", async () => {
|
||||
const tree = await mergeTrees.call(
|
||||
null,
|
||||
{
|
||||
a: 1,
|
||||
b: 2,
|
||||
},
|
||||
{
|
||||
b: 3,
|
||||
c: 4,
|
||||
}
|
||||
);
|
||||
// @ts-ignore
|
||||
assert.deepEqual(await Tree.plain(tree), {
|
||||
a: 1,
|
||||
b: 3,
|
||||
c: 4,
|
||||
});
|
||||
});
|
||||
|
||||
test("if all arguments are plain objects, result is a plain object", async () => {
|
||||
const result = await mergeTrees.call(
|
||||
null,
|
||||
{
|
||||
a: 1,
|
||||
b: 2,
|
||||
},
|
||||
{
|
||||
b: 3,
|
||||
c: 4,
|
||||
}
|
||||
);
|
||||
assert.deepEqual(result, {
|
||||
a: 1,
|
||||
b: 3,
|
||||
c: 4,
|
||||
});
|
||||
});
|
||||
|
||||
test("if all arguments are arrays, result is an array", async () => {
|
||||
const result = await mergeTrees.call(null, [1, 2], [3, 4]);
|
||||
assert.deepEqual(result, [1, 2, 3, 4]);
|
||||
});
|
||||
});
|
||||
358
node_modules/@weborigami/language/test/runtime/ops.test.js
generated
vendored
Normal file
358
node_modules/@weborigami/language/test/runtime/ops.test.js
generated
vendored
Normal file
@@ -0,0 +1,358 @@
|
||||
import { ObjectTree } from "@weborigami/async-tree";
|
||||
import assert from "node:assert";
|
||||
import { describe, test } from "node:test";
|
||||
|
||||
import { evaluate, ops } from "../../src/runtime/internal.js";
|
||||
|
||||
describe("ops", () => {
|
||||
test("ops.addition adds two numbers", async () => {
|
||||
assert.strictEqual(ops.addition(2, 2), 4);
|
||||
assert.strictEqual(ops.addition(2, true), 3);
|
||||
});
|
||||
|
||||
test("ops.addition concatenates two strings", async () => {
|
||||
assert.strictEqual(ops.addition("hello ", "everyone"), "hello everyone");
|
||||
assert.strictEqual(
|
||||
ops.addition("2001", ": A Space Odyssey"),
|
||||
"2001: A Space Odyssey"
|
||||
);
|
||||
});
|
||||
|
||||
test("ops.array creates an array", async () => {
|
||||
const code = createCode([ops.array, 1, 2, 3]);
|
||||
const result = await evaluate.call(null, code);
|
||||
assert.deepEqual(result, [1, 2, 3]);
|
||||
});
|
||||
|
||||
test("ops.bitwiseAnd", () => {
|
||||
assert.strictEqual(ops.bitwiseAnd(5, 3), 1);
|
||||
});
|
||||
|
||||
test("ops.bitwiseNot", () => {
|
||||
assert.strictEqual(ops.bitwiseNot(5), -6);
|
||||
assert.strictEqual(ops.bitwiseNot(-3), 2);
|
||||
});
|
||||
|
||||
test("ops.bitwiseOr", () => {
|
||||
assert.strictEqual(ops.bitwiseOr(5, 3), 7);
|
||||
});
|
||||
|
||||
test("ops.bitwiseXor", () => {
|
||||
assert.strictEqual(ops.bitwiseXor(5, 3), 6);
|
||||
});
|
||||
|
||||
test("ops.builtin gets a value from the top of the scope chain", async () => {
|
||||
const root = new ObjectTree({
|
||||
a: 1,
|
||||
});
|
||||
const tree = new ObjectTree({});
|
||||
tree.parent = root;
|
||||
const code = createCode([ops.builtin, "a"]);
|
||||
const result = await evaluate.call(tree, code);
|
||||
assert.strictEqual(result, 1);
|
||||
});
|
||||
|
||||
test("ops.comma returns the last value", async () => {
|
||||
const code = createCode([ops.comma, 1, 2, 3]);
|
||||
const result = await evaluate.call(null, code);
|
||||
assert.strictEqual(result, 3);
|
||||
});
|
||||
|
||||
test("ops.concat concatenates tree value text", async () => {
|
||||
const scope = new ObjectTree({
|
||||
name: "world",
|
||||
});
|
||||
|
||||
const code = createCode([ops.concat, "Hello, ", [ops.scope, "name"], "."]);
|
||||
|
||||
const result = await evaluate.call(scope, code);
|
||||
assert.strictEqual(result, "Hello, world.");
|
||||
});
|
||||
|
||||
test("ops.conditional", async () => {
|
||||
assert.strictEqual(await ops.conditional(true, trueFn, falseFn), true);
|
||||
assert.strictEqual(await ops.conditional(true, falseFn, trueFn), false);
|
||||
assert.strictEqual(await ops.conditional(false, trueFn, falseFn), false);
|
||||
assert.strictEqual(await ops.conditional(false, falseFn, trueFn), true);
|
||||
|
||||
// Short-circuiting
|
||||
assert.strictEqual(await ops.conditional(false, errorFn, trueFn), true);
|
||||
});
|
||||
|
||||
test("ops.division divides two numbers", async () => {
|
||||
assert.strictEqual(ops.division(12, 2), 6);
|
||||
assert.strictEqual(ops.division(3, 2), 1.5);
|
||||
assert.strictEqual(ops.division(6, "3"), 2);
|
||||
assert.strictEqual(ops.division(2, 0), Infinity);
|
||||
});
|
||||
|
||||
test("ops.equal", () => {
|
||||
assert(ops.equal(1, 1));
|
||||
assert(!ops.equal(1, 2));
|
||||
assert(ops.equal("1", 1));
|
||||
assert(ops.equal("1", "1"));
|
||||
assert(ops.equal(null, undefined));
|
||||
});
|
||||
|
||||
test("ops.exponentiation", () => {
|
||||
assert.strictEqual(ops.exponentiation(2, 3), 8);
|
||||
assert.strictEqual(ops.exponentiation(2, 0), 1);
|
||||
});
|
||||
|
||||
test("ops.external looks up a value in scope and memoizes it", async () => {
|
||||
let count = 0;
|
||||
const tree = new ObjectTree({
|
||||
get count() {
|
||||
return ++count;
|
||||
},
|
||||
});
|
||||
const code = createCode([ops.external, "count", {}]);
|
||||
const result = await evaluate.call(tree, code);
|
||||
assert.strictEqual(result, 1);
|
||||
const result2 = await evaluate.call(tree, code);
|
||||
assert.strictEqual(result2, 1);
|
||||
});
|
||||
|
||||
test("ops.greaterThan", () => {
|
||||
assert(ops.greaterThan(5, 3));
|
||||
assert(!ops.greaterThan(3, 3));
|
||||
assert(ops.greaterThan("ab", "aa"));
|
||||
});
|
||||
|
||||
test("ops.greaterThanOrEqual", () => {
|
||||
assert(ops.greaterThanOrEqual(5, 3));
|
||||
assert(ops.greaterThanOrEqual(3, 3));
|
||||
assert(ops.greaterThanOrEqual("ab", "aa"));
|
||||
});
|
||||
|
||||
test("ops.inherited searches inherited scope", async () => {
|
||||
const parent = new ObjectTree({
|
||||
a: 1, // This is the inherited value we want
|
||||
});
|
||||
/** @type {any} */
|
||||
const child = new ObjectTree({
|
||||
a: 2, // Should be ignored
|
||||
});
|
||||
child.parent = parent;
|
||||
const code = createCode([ops.inherited, "a"]);
|
||||
const result = await evaluate.call(child, code);
|
||||
assert.strictEqual(result, 1);
|
||||
});
|
||||
|
||||
test("ops.lambda defines a function with no inputs", async () => {
|
||||
const code = createCode([ops.lambda, [], [ops.literal, "result"]]);
|
||||
const fn = await evaluate.call(null, code);
|
||||
const result = await fn.call();
|
||||
assert.strictEqual(result, "result");
|
||||
});
|
||||
|
||||
test("ops.lambda defines a function with underscore input", async () => {
|
||||
const scope = new ObjectTree({
|
||||
message: "Hello",
|
||||
});
|
||||
|
||||
const code = createCode([ops.lambda, ["_"], [ops.scope, "message"]]);
|
||||
|
||||
const fn = await evaluate.call(scope, code);
|
||||
const result = await fn.call(scope);
|
||||
assert.strictEqual(result, "Hello");
|
||||
});
|
||||
|
||||
test("ops.lambda adds input parameters to scope", async () => {
|
||||
const code = createCode([
|
||||
ops.lambda,
|
||||
["a", "b"],
|
||||
[ops.concat, [ops.scope, "b"], [ops.scope, "a"]],
|
||||
]);
|
||||
const fn = await evaluate.call(null, code);
|
||||
const result = await fn("x", "y");
|
||||
assert.strictEqual(result, "yx");
|
||||
});
|
||||
|
||||
test("ops.lessThan", () => {
|
||||
assert(!ops.lessThan(5, 3));
|
||||
assert(!ops.lessThan(3, 3));
|
||||
assert(ops.lessThan("aa", "ab"));
|
||||
});
|
||||
|
||||
test("ops.lessThanOrEqual", () => {
|
||||
assert(!ops.lessThanOrEqual(5, 3));
|
||||
assert(ops.lessThanOrEqual(3, 3));
|
||||
assert(ops.lessThanOrEqual("aa", "ab"));
|
||||
});
|
||||
|
||||
test("ops.logicalAnd", async () => {
|
||||
assert.strictEqual(await ops.logicalAnd(true, trueFn), true);
|
||||
assert.strictEqual(await ops.logicalAnd(true, falseFn), false);
|
||||
assert.strictEqual(await ops.logicalAnd(false, trueFn), false);
|
||||
assert.strictEqual(await ops.logicalAnd(false, falseFn), false);
|
||||
|
||||
assert.strictEqual(await ops.logicalAnd(true, "hi"), "hi");
|
||||
|
||||
// Short-circuiting
|
||||
assert.strictEqual(await ops.logicalAnd(false, errorFn), false);
|
||||
assert.strictEqual(await ops.logicalAnd(0, true), 0);
|
||||
});
|
||||
|
||||
test("ops.logicalNot", async () => {
|
||||
assert.strictEqual(await ops.logicalNot(true), false);
|
||||
assert.strictEqual(await ops.logicalNot(false), true);
|
||||
assert.strictEqual(await ops.logicalNot(0), true);
|
||||
assert.strictEqual(await ops.logicalNot(1), false);
|
||||
});
|
||||
|
||||
test("ops.logicalOr", async () => {
|
||||
assert.strictEqual(await ops.logicalOr(true, trueFn), true);
|
||||
assert.strictEqual(await ops.logicalOr(true, falseFn), true);
|
||||
assert.strictEqual(await ops.logicalOr(false, trueFn), true);
|
||||
assert.strictEqual(await ops.logicalOr(false, falseFn), false);
|
||||
|
||||
assert.strictEqual(await ops.logicalOr(false, "hi"), "hi");
|
||||
|
||||
// Short-circuiting
|
||||
assert.strictEqual(await ops.logicalOr(true, errorFn), true);
|
||||
});
|
||||
|
||||
test("ops.multiplication multiplies two numbers", async () => {
|
||||
assert.strictEqual(ops.multiplication(3, 4), 12);
|
||||
assert.strictEqual(ops.multiplication(-3, 4), -12);
|
||||
assert.strictEqual(ops.multiplication("3", 2), 6);
|
||||
assert.strictEqual(ops.multiplication("foo", 2), NaN);
|
||||
});
|
||||
|
||||
test("ops.notEqual", () => {
|
||||
assert(!ops.notEqual(1, 1));
|
||||
assert(ops.notEqual(1, 2));
|
||||
assert(!ops.notEqual("1", 1));
|
||||
assert(!ops.notEqual("1", "1"));
|
||||
assert(!ops.notEqual(null, undefined));
|
||||
});
|
||||
|
||||
test("ops.notStrictEqual", () => {
|
||||
assert(!ops.notStrictEqual(1, 1));
|
||||
assert(ops.notStrictEqual(1, 2));
|
||||
assert(ops.notStrictEqual("1", 1));
|
||||
assert(!ops.notStrictEqual("1", "1"));
|
||||
assert(ops.notStrictEqual(null, undefined));
|
||||
});
|
||||
|
||||
test("ops.nullishCoalescing", async () => {
|
||||
assert.strictEqual(await ops.nullishCoalescing(1, falseFn), 1);
|
||||
assert.strictEqual(await ops.nullishCoalescing(null, trueFn), true);
|
||||
assert.strictEqual(await ops.nullishCoalescing(undefined, trueFn), true);
|
||||
|
||||
// Short-circuiting
|
||||
assert.strictEqual(await ops.nullishCoalescing(1, errorFn), 1);
|
||||
});
|
||||
|
||||
test("ops.object instantiates an object", async () => {
|
||||
const scope = new ObjectTree({
|
||||
upper: (s) => s.toUpperCase(),
|
||||
});
|
||||
|
||||
const code = createCode([
|
||||
ops.object,
|
||||
["hello", [[ops.scope, "upper"], "hello"]],
|
||||
["world", [[ops.scope, "upper"], "world"]],
|
||||
]);
|
||||
|
||||
const result = await evaluate.call(scope, code);
|
||||
assert.strictEqual(result.hello, "HELLO");
|
||||
assert.strictEqual(result.world, "WORLD");
|
||||
});
|
||||
|
||||
test("ops.object instantiates an array", async () => {
|
||||
const scope = new ObjectTree({
|
||||
upper: (s) => s.toUpperCase(),
|
||||
});
|
||||
const code = createCode([
|
||||
ops.array,
|
||||
"Hello",
|
||||
1,
|
||||
[[ops.scope, "upper"], "world"],
|
||||
]);
|
||||
const result = await evaluate.call(scope, code);
|
||||
assert.deepEqual(result, ["Hello", 1, "WORLD"]);
|
||||
});
|
||||
|
||||
test("ops.remainder calculates the remainder of two numbers", async () => {
|
||||
assert.strictEqual(ops.remainder(13, 5), 3);
|
||||
assert.strictEqual(ops.remainder(-13, 5), -3);
|
||||
assert.strictEqual(ops.remainder(4, 2), 0);
|
||||
assert.strictEqual(ops.remainder(-4, 2), -0);
|
||||
});
|
||||
|
||||
test("ops.shiftLeft", () => {
|
||||
assert.strictEqual(ops.shiftLeft(5, 2), 20);
|
||||
});
|
||||
|
||||
test("ops.shiftRightSigned", () => {
|
||||
assert.strictEqual(ops.shiftRightSigned(20, 2), 5);
|
||||
assert.strictEqual(ops.shiftRightSigned(-20, 2), -5);
|
||||
});
|
||||
|
||||
test("ops.shiftRightUnsigned", () => {
|
||||
assert.strictEqual(ops.shiftRightUnsigned(20, 2), 5);
|
||||
assert.strictEqual(ops.shiftRightUnsigned(-5, 2), 1073741822);
|
||||
});
|
||||
|
||||
test("ops.strictEqual", () => {
|
||||
assert(ops.strictEqual(1, 1));
|
||||
assert(!ops.strictEqual(1, 2));
|
||||
assert(!ops.strictEqual("1", 1));
|
||||
assert(ops.strictEqual("1", "1"));
|
||||
assert(!ops.strictEqual(null, undefined));
|
||||
assert(ops.strictEqual(null, null));
|
||||
assert(ops.strictEqual(undefined, undefined));
|
||||
});
|
||||
|
||||
test("ops.subtraction subtracts two numbers", async () => {
|
||||
assert.strictEqual(ops.subtraction(5, 3), 2);
|
||||
assert.strictEqual(ops.subtraction(3.5, 5), -1.5);
|
||||
assert.strictEqual(ops.subtraction(5, "hello"), NaN);
|
||||
assert.strictEqual(ops.subtraction(5, true), 4);
|
||||
});
|
||||
|
||||
test("ops.unaryMinus", () => {
|
||||
assert.strictEqual(ops.unaryMinus(4), -4);
|
||||
assert.strictEqual(ops.unaryMinus(-4), 4);
|
||||
});
|
||||
|
||||
test("ops.unaryPlus", () => {
|
||||
assert.strictEqual(ops.unaryPlus(1), 1);
|
||||
assert.strictEqual(ops.unaryPlus(-1), -1);
|
||||
assert.strictEqual(ops.unaryPlus(""), 0);
|
||||
});
|
||||
|
||||
test("ops.unpack unpacks a value", async () => {
|
||||
const fixture = new String("packed");
|
||||
/** @type {any} */ (fixture).unpack = async () => "unpacked";
|
||||
const result = await ops.unpack.call(null, fixture);
|
||||
assert.strictEqual(result, "unpacked");
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @returns {import("../../index.ts").Code}
|
||||
*/
|
||||
function createCode(array) {
|
||||
const code = array;
|
||||
/** @type {any} */ (code).location = {
|
||||
source: {
|
||||
text: "",
|
||||
},
|
||||
};
|
||||
return code;
|
||||
}
|
||||
|
||||
function errorFn() {
|
||||
throw new Error("This should not be called");
|
||||
}
|
||||
|
||||
function falseFn() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function trueFn() {
|
||||
return true;
|
||||
}
|
||||
10
node_modules/@weborigami/language/test/runtime/taggedTemplate.test.js
generated
vendored
Normal file
10
node_modules/@weborigami/language/test/runtime/taggedTemplate.test.js
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import assert from "node:assert";
|
||||
import { describe, test } from "node:test";
|
||||
import taggedTemplate from "../../src/runtime/taggedTemplate.js";
|
||||
|
||||
describe("taggedTemplate", () => {
|
||||
test("joins strings and values together", () => {
|
||||
const result = taggedTemplate`a ${"b"} c`;
|
||||
assert.equal(result, "a b c");
|
||||
});
|
||||
});
|
||||
21
node_modules/@weborigami/language/test/runtime/typos.test.js
generated
vendored
Normal file
21
node_modules/@weborigami/language/test/runtime/typos.test.js
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
import assert from "node:assert";
|
||||
import { describe, test } from "node:test";
|
||||
import { isTypo, typos } from "../../src/runtime/typos.js";
|
||||
|
||||
describe("typos", () => {
|
||||
test("isTypo", () => {
|
||||
assert(isTypo("cat", "bat")); // substitution
|
||||
assert(isTypo("cat", "cats")); // insertion
|
||||
assert(isTypo("cat", "cast")); // insertion
|
||||
assert(isTypo("cat", "at")); // deletion
|
||||
assert(isTypo("cat", "ca")); // deletion
|
||||
assert(isTypo("cat", "cta")); // transposition
|
||||
assert(isTypo("cat", "act")); // transposition
|
||||
assert(!isTypo("cat", "dog")); // more than 1 edit
|
||||
});
|
||||
|
||||
test("typos", () => {
|
||||
const result = typos("cas", ["ask", "cat", "cast", "cats", "cart"]);
|
||||
assert.deepEqual(result, ["cat", "cast", "cats"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user