run npm install to generate a package lock

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

View File

@@ -0,0 +1 @@
This folder defines expression tests in YAML files so that we can programmatically test the evaluation of the expressions in both JavaScript and Origami.

View 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"

View 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"

View 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"

View 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"

View 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);
}

File diff suppressed because it is too large Load Diff

View 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;
}
}

View 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");
});

View 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");
});

View 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");
});

View 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");
});

View 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);

View 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;
}

View 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);
});
});

View 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 });
}

View 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;
}

View 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");
});
});

View File

@@ -0,0 +1 @@
export default () => "bar";

View File

@@ -0,0 +1 @@
Hello, world.

View File

@@ -0,0 +1 @@
Foo

View File

@@ -0,0 +1,3 @@
export default function (name) {
return `Hello, ${name}.`;
}

View File

@@ -0,0 +1,5 @@
{
"a": "Hello, a.",
"b": "Hello, b.",
"c": "Hello, c."
}

View File

@@ -0,0 +1,3 @@
export default function () {
return "Hello, world.";
}

View File

@@ -0,0 +1 @@
"Hello, world."

View File

@@ -0,0 +1,5 @@
import { ObjectTree } from "@weborigami/async-tree";
export default new ObjectTree({
a: "Hello, a.",
b: "Hello, b.",
});

View File

@@ -0,0 +1,4 @@
Greetings:
{{ map(=`Hello, {{ _ }}.
`)(names.yaml) }}

View 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>

View File

@@ -0,0 +1,3 @@
- Alice
- Bob
- Carol

View File

@@ -0,0 +1 @@
Hello, world.

View File

@@ -0,0 +1 @@
["a", "b", "c"]

View 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",
});
});
});

View 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;
}

View 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]);
});
});

View 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;
}

View 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");
});
});

View 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"]);
});
});