@rhost/testkit 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/node_modules/@ursamu/mushcode/.github/workflows/publish.yml +36 -0
- package/node_modules/@ursamu/mushcode/LICENSE +21 -0
- package/node_modules/@ursamu/mushcode/README.md +110 -0
- package/node_modules/@ursamu/mushcode/_dist/mod.d.ts +36 -0
- package/node_modules/@ursamu/mushcode/_dist/mod.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/parser/mod.d.ts +41 -0
- package/node_modules/@ursamu/mushcode/_dist/parser/mod.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/analyze/commands.d.ts +15 -0
- package/node_modules/@ursamu/mushcode/_dist/src/analyze/commands.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/analyze/deps.d.ts +18 -0
- package/node_modules/@ursamu/mushcode/_dist/src/analyze/deps.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/analyze/mod.d.ts +20 -0
- package/node_modules/@ursamu/mushcode/_dist/src/analyze/mod.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/analyze/tags.d.ts +6 -0
- package/node_modules/@ursamu/mushcode/_dist/src/analyze/tags.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/eval/context.d.ts +85 -0
- package/node_modules/@ursamu/mushcode/_dist/src/eval/context.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/eval/engine.d.ts +48 -0
- package/node_modules/@ursamu/mushcode/_dist/src/eval/engine.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/eval/mod.d.ts +26 -0
- package/node_modules/@ursamu/mushcode/_dist/src/eval/mod.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/eval/stdlib/mod.d.ts +3 -0
- package/node_modules/@ursamu/mushcode/_dist/src/eval/stdlib/mod.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/lint/mod.d.ts +38 -0
- package/node_modules/@ursamu/mushcode/_dist/src/lint/mod.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/print/mod.d.ts +18 -0
- package/node_modules/@ursamu/mushcode/_dist/src/print/mod.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/print/printer.d.ts +15 -0
- package/node_modules/@ursamu/mushcode/_dist/src/print/printer.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/traverse/mod.d.ts +19 -0
- package/node_modules/@ursamu/mushcode/_dist/src/traverse/mod.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/traverse/transform.d.ts +27 -0
- package/node_modules/@ursamu/mushcode/_dist/src/traverse/transform.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/_dist/src/traverse/walk.d.ts +27 -0
- package/node_modules/@ursamu/mushcode/_dist/src/traverse/walk.d.ts.map +1 -0
- package/node_modules/@ursamu/mushcode/deno.json +26 -0
- package/node_modules/@ursamu/mushcode/deno.lock +42 -0
- package/node_modules/@ursamu/mushcode/docs/analyze.md +145 -0
- package/node_modules/@ursamu/mushcode/docs/eval.md +312 -0
- package/node_modules/@ursamu/mushcode/docs/lint.md +152 -0
- package/node_modules/@ursamu/mushcode/docs/parser.md +196 -0
- package/node_modules/@ursamu/mushcode/docs/print.md +84 -0
- package/node_modules/@ursamu/mushcode/docs/stdlib.md +418 -0
- package/node_modules/@ursamu/mushcode/docs/traverse.md +167 -0
- package/node_modules/@ursamu/mushcode/grammar/mux-softcode.pegjs +781 -0
- package/node_modules/@ursamu/mushcode/mod.js +44 -0
- package/node_modules/@ursamu/mushcode/mod.js.map +1 -0
- package/node_modules/@ursamu/mushcode/mod.ts +63 -0
- package/node_modules/@ursamu/mushcode/package.json +38 -0
- package/node_modules/@ursamu/mushcode/parser/mod.js +47 -0
- package/node_modules/@ursamu/mushcode/parser/mod.js.map +1 -0
- package/node_modules/@ursamu/mushcode/parser/mod.ts +99 -0
- package/node_modules/@ursamu/mushcode/parser/mux-softcode.js +3833 -0
- package/node_modules/@ursamu/mushcode/parser/mux-softcode.mjs +3837 -0
- package/node_modules/@ursamu/mushcode/src/analyze/commands.js +29 -0
- package/node_modules/@ursamu/mushcode/src/analyze/commands.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/analyze/commands.ts +46 -0
- package/node_modules/@ursamu/mushcode/src/analyze/deps.js +45 -0
- package/node_modules/@ursamu/mushcode/src/analyze/deps.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/analyze/deps.ts +51 -0
- package/node_modules/@ursamu/mushcode/src/analyze/mod.js +18 -0
- package/node_modules/@ursamu/mushcode/src/analyze/mod.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/analyze/mod.ts +20 -0
- package/node_modules/@ursamu/mushcode/src/analyze/tags.js +11 -0
- package/node_modules/@ursamu/mushcode/src/analyze/tags.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/analyze/tags.ts +11 -0
- package/node_modules/@ursamu/mushcode/src/eval/context.js +22 -0
- package/node_modules/@ursamu/mushcode/src/eval/context.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/context.ts +177 -0
- package/node_modules/@ursamu/mushcode/src/eval/engine.js +238 -0
- package/node_modules/@ursamu/mushcode/src/eval/engine.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/engine.ts +276 -0
- package/node_modules/@ursamu/mushcode/src/eval/mod.js +25 -0
- package/node_modules/@ursamu/mushcode/src/eval/mod.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/mod.ts +31 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/compare.js +56 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/compare.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/compare.ts +16 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/db.js +91 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/db.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/db.ts +104 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/iter.js +91 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/iter.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/iter.ts +98 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/logic.js +79 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/logic.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/logic.ts +84 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/math.js +120 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/math.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/math.ts +115 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/mod.js +17 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/mod.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/mod.ts +19 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/register.js +28 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/register.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/register.ts +31 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/string.js +153 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/string.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/eval/stdlib/string.ts +154 -0
- package/node_modules/@ursamu/mushcode/src/lint/builtin_arities.js +212 -0
- package/node_modules/@ursamu/mushcode/src/lint/builtin_arities.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/lint/builtin_arities.ts +68 -0
- package/node_modules/@ursamu/mushcode/src/lint/mod.js +60 -0
- package/node_modules/@ursamu/mushcode/src/lint/mod.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/lint/mod.ts +96 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/arg_count.js +37 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/arg_count.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/arg_count.ts +44 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/iter_var_outside_iter.js +55 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/iter_var_outside_iter.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/iter_var_outside_iter.ts +60 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/missing_wildcard.js +31 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/missing_wildcard.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/missing_wildcard.ts +40 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/register_before_set.js +59 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/register_before_set.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/lint/rules/register_before_set.ts +64 -0
- package/node_modules/@ursamu/mushcode/src/print/lock_printer.js +43 -0
- package/node_modules/@ursamu/mushcode/src/print/lock_printer.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/print/lock_printer.ts +41 -0
- package/node_modules/@ursamu/mushcode/src/print/mod.js +17 -0
- package/node_modules/@ursamu/mushcode/src/print/mod.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/print/mod.ts +18 -0
- package/node_modules/@ursamu/mushcode/src/print/printer.js +91 -0
- package/node_modules/@ursamu/mushcode/src/print/printer.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/print/printer.ts +132 -0
- package/node_modules/@ursamu/mushcode/src/traverse/child_slots.js +129 -0
- package/node_modules/@ursamu/mushcode/src/traverse/child_slots.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/traverse/child_slots.ts +51 -0
- package/node_modules/@ursamu/mushcode/src/traverse/mod.js +17 -0
- package/node_modules/@ursamu/mushcode/src/traverse/mod.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/traverse/mod.ts +19 -0
- package/node_modules/@ursamu/mushcode/src/traverse/transform.js +70 -0
- package/node_modules/@ursamu/mushcode/src/traverse/transform.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/traverse/transform.ts +84 -0
- package/node_modules/@ursamu/mushcode/src/traverse/walk.js +55 -0
- package/node_modules/@ursamu/mushcode/src/traverse/walk.js.map +1 -0
- package/node_modules/@ursamu/mushcode/src/traverse/walk.ts +82 -0
- package/node_modules/@ursamu/mushcode/tests/01-literals.test.ts +105 -0
- package/node_modules/@ursamu/mushcode/tests/02-substitutions.test.ts +145 -0
- package/node_modules/@ursamu/mushcode/tests/03-function-calls.test.ts +184 -0
- package/node_modules/@ursamu/mushcode/tests/04-eval-blocks.test.ts +110 -0
- package/node_modules/@ursamu/mushcode/tests/05-braced-strings.test.ts +119 -0
- package/node_modules/@ursamu/mushcode/tests/06-commands.test.ts +222 -0
- package/node_modules/@ursamu/mushcode/tests/07-dollar-patterns.test.ts +156 -0
- package/node_modules/@ursamu/mushcode/tests/08-lock-expressions.test.ts +159 -0
- package/node_modules/@ursamu/mushcode/tests/09-edge-cases.test.ts +162 -0
- package/node_modules/@ursamu/mushcode/tests/10-regression.test.ts +211 -0
- package/node_modules/@ursamu/mushcode/tests/11-tags.test.ts +357 -0
- package/node_modules/@ursamu/mushcode/tests/12-locations.test.ts +162 -0
- package/node_modules/@ursamu/mushcode/tests/13-eval.test.ts +389 -0
- package/node_modules/@ursamu/mushcode/tests/analyze.test.ts +194 -0
- package/node_modules/@ursamu/mushcode/tests/helpers.ts +69 -0
- package/node_modules/@ursamu/mushcode/tests/lint.test.ts +232 -0
- package/node_modules/@ursamu/mushcode/tests/print.test.ts +204 -0
- package/node_modules/@ursamu/mushcode/tests/traverse.test.ts +211 -0
- package/package.json +4 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// child_slots.ts — authoritative child-field registry for every AST node type
|
|
3
|
+
//
|
|
4
|
+
// Used by walk() and transform() to know exactly which fields hold child nodes,
|
|
5
|
+
// whether each field is a single node (possibly null) or an array of nodes.
|
|
6
|
+
//
|
|
7
|
+
// Leaf types (Literal, Escape, Substitution, SpecialVar, Wildcard, TagRef,
|
|
8
|
+
// LockMe, LockDbref, LockFlagCheck, LockTypeCheck, LockAttrCheck,
|
|
9
|
+
// LockPlayerName) have no entry — the walker treats missing entries as leaves.
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export type SlotKind = "single" | "array";
|
|
13
|
+
|
|
14
|
+
export interface SlotDef {
|
|
15
|
+
field: string;
|
|
16
|
+
kind: SlotKind;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const CHILD_SLOTS: Readonly<Record<string, SlotDef[]>> = {
|
|
20
|
+
// ── Commands ────────────────────────────────────────────────────────────────
|
|
21
|
+
CommandList: [{ field: "commands", kind: "array" }],
|
|
22
|
+
AtCommand: [{ field: "object", kind: "single" }, // Text | null
|
|
23
|
+
{ field: "value", kind: "single" }], // AtCmdValue | null
|
|
24
|
+
AttributeSet: [{ field: "object", kind: "single" }, // Text
|
|
25
|
+
{ field: "value", kind: "single" }], // AtCmdValue | null
|
|
26
|
+
UserCommand: [{ field: "parts", kind: "array" }],
|
|
27
|
+
|
|
28
|
+
// ── Patterns ────────────────────────────────────────────────────────────────
|
|
29
|
+
DollarPattern: [{ field: "pattern", kind: "single" },
|
|
30
|
+
{ field: "action", kind: "single" }],
|
|
31
|
+
ListenPattern: [{ field: "pattern", kind: "single" },
|
|
32
|
+
{ field: "action", kind: "single" }],
|
|
33
|
+
PatternAlts: [{ field: "patterns", kind: "array" }],
|
|
34
|
+
Pattern: [{ field: "parts", kind: "array" }],
|
|
35
|
+
|
|
36
|
+
// ── Expression containers ───────────────────────────────────────────────────
|
|
37
|
+
EvalBlock: [{ field: "parts", kind: "array" }],
|
|
38
|
+
BracedString: [{ field: "parts", kind: "array" }],
|
|
39
|
+
FunctionCall: [{ field: "args", kind: "array" }],
|
|
40
|
+
Arg: [{ field: "parts", kind: "array" }],
|
|
41
|
+
Text: [{ field: "parts", kind: "array" }],
|
|
42
|
+
|
|
43
|
+
// ── Lock expressions ────────────────────────────────────────────────────────
|
|
44
|
+
LockOr: [{ field: "operands", kind: "array" }],
|
|
45
|
+
LockAnd: [{ field: "operands", kind: "array" }],
|
|
46
|
+
LockNot: [{ field: "operand", kind: "single" }],
|
|
47
|
+
|
|
48
|
+
// Leaf types (no entry): Literal, Escape, Substitution, SpecialVar, Wildcard,
|
|
49
|
+
// TagRef, LockMe, LockDbref, LockFlagCheck, LockTypeCheck, LockAttrCheck,
|
|
50
|
+
// LockPlayerName
|
|
51
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* Depth-first AST traversal and tree transformation utilities:
|
|
4
|
+
* `walk()`, `transform()`, `findAll()`, `findFirst()`, `findFirstOrNull()`.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { parse } from "@ursamu/mushcode/parse";
|
|
9
|
+
* import { findAll } from "@ursamu/mushcode/traverse";
|
|
10
|
+
*
|
|
11
|
+
* const ast = parse("[add(1,[mul(2,3)])]");
|
|
12
|
+
* const calls = findAll(ast, "FunctionCall");
|
|
13
|
+
* console.log(calls.map(n => n.name)); // ["add", "mul"]
|
|
14
|
+
* ```
|
|
15
|
+
*/ export { walk, findAll, findFirst, findFirstOrNull } from "./walk.js";
|
|
16
|
+
export { transform } from "./transform.js";
|
|
17
|
+
//# sourceMappingURL=mod.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mod.js","sources":["./mod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;CAcC,GAED,SAAS,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,eAAe,oBAA2B;AAE7E,SAAS,SAAS,yBAAgE"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* Depth-first AST traversal and tree transformation utilities:
|
|
4
|
+
* `walk()`, `transform()`, `findAll()`, `findFirst()`, `findFirstOrNull()`.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { parse } from "@ursamu/mushcode/parse";
|
|
9
|
+
* import { findAll } from "@ursamu/mushcode/traverse";
|
|
10
|
+
*
|
|
11
|
+
* const ast = parse("[add(1,[mul(2,3)])]");
|
|
12
|
+
* const calls = findAll(ast, "FunctionCall");
|
|
13
|
+
* console.log(calls.map(n => n.name)); // ["add", "mul"]
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export type { Visitor } from "./walk.js";
|
|
17
|
+
export { walk, findAll, findFirst, findFirstOrNull } from "./walk.js";
|
|
18
|
+
export type { Transformer } from "./transform.js";
|
|
19
|
+
export { transform } from "./transform.js";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { CHILD_SLOTS } from "./child_slots.js";
|
|
2
|
+
// ── transform ─────────────────────────────────────────────────────────────────
|
|
3
|
+
/**
|
|
4
|
+
* Produce a new tree by applying `fn` to every node top-down.
|
|
5
|
+
*
|
|
6
|
+
* The function is called on each node before its children are processed.
|
|
7
|
+
* Returning a replacement node causes that replacement's children to be
|
|
8
|
+
* processed next (not the original children).
|
|
9
|
+
*
|
|
10
|
+
* The original tree is never mutated; a new object is returned whenever
|
|
11
|
+
* any node in the subtree changes.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // Replace every TagRef with a Literal placeholder
|
|
15
|
+
* const out = transform(ast, (n) => {
|
|
16
|
+
* if (n.type === "TagRef") return { type: "Literal", value: `<tag:${n.name}>` };
|
|
17
|
+
* });
|
|
18
|
+
*/ export function transform(root, fn) {
|
|
19
|
+
return transformNode(root, fn) ?? root;
|
|
20
|
+
}
|
|
21
|
+
function transformNode(node, fn) {
|
|
22
|
+
// 1. Apply fn to this node first (top-down)
|
|
23
|
+
const replacement = fn(node);
|
|
24
|
+
if (replacement === null) return null; // removal signal
|
|
25
|
+
const current = replacement ?? node;
|
|
26
|
+
// 2. Recurse into children of `current`
|
|
27
|
+
const slots = CHILD_SLOTS[current.type] ?? [];
|
|
28
|
+
if (slots.length === 0) return current; // leaf — nothing to recurse
|
|
29
|
+
// deno-lint-ignore no-explicit-any
|
|
30
|
+
const patches = {};
|
|
31
|
+
let changed = replacement !== undefined; // track whether we need a new object
|
|
32
|
+
for (const { field, kind } of slots){
|
|
33
|
+
// deno-lint-ignore no-explicit-any
|
|
34
|
+
const val = current[field];
|
|
35
|
+
if (kind === "array") {
|
|
36
|
+
if (!Array.isArray(val)) continue;
|
|
37
|
+
const newArr = [];
|
|
38
|
+
let arrChanged = false;
|
|
39
|
+
for (const item of val){
|
|
40
|
+
if (!item || typeof item !== "object" || !item.type) {
|
|
41
|
+
newArr.push(item);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const result = transformNode(item, fn);
|
|
45
|
+
if (result === null) {
|
|
46
|
+
arrChanged = true;
|
|
47
|
+
continue;
|
|
48
|
+
} // removed
|
|
49
|
+
newArr.push(result);
|
|
50
|
+
if (result !== item) arrChanged = true;
|
|
51
|
+
}
|
|
52
|
+
if (arrChanged) {
|
|
53
|
+
patches[field] = newArr;
|
|
54
|
+
changed = true;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
if (!val || typeof val !== "object" || !val.type) continue;
|
|
58
|
+
const result = transformNode(val, fn);
|
|
59
|
+
if (result !== val) {
|
|
60
|
+
patches[field] = result ?? null;
|
|
61
|
+
changed = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return changed ? {
|
|
66
|
+
...current,
|
|
67
|
+
...patches
|
|
68
|
+
} : current;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=transform.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transform.js","sources":["./transform.ts"],"names":[],"mappings":"AACA,SAAS,WAAW,2BAA2B;AAe/C,iFAAiF;AAEjF;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAAS,UAAU,IAAa,EAAE,EAAe;EACtD,OAAO,cAAc,MAAM,OAAO;AACpC;AAEA,SAAS,cAAc,IAAa,EAAE,EAAe;EACnD,4CAA4C;EAC5C,MAAM,cAAc,GAAG;EACvB,IAAI,gBAAgB,MAAM,OAAO,MAAM,iBAAiB;EACxD,MAAM,UAAU,eAAe;EAE/B,wCAAwC;EACxC,MAAM,QAAQ,WAAW,CAAC,QAAQ,IAAI,CAAC,IAAI,EAAE;EAC7C,IAAI,MAAM,MAAM,KAAK,GAAG,OAAO,SAAS,4BAA4B;EAEpE,mCAAmC;EACnC,MAAM,UAA+B,CAAC;EACtC,IAAI,UAAU,gBAAgB,WAAW,qCAAqC;EAE9E,KAAK,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,MAAO;IACnC,mCAAmC;IACnC,MAAM,MAAM,AAAC,OAAe,CAAC,MAAM;IAEnC,IAAI,SAAS,SAAS;MACpB,IAAI,CAAC,MAAM,OAAO,CAAC,MAAM;MACzB,MAAM,SAAoB,EAAE;MAC5B,IAAI,aAAa;MACjB,KAAK,MAAM,QAAQ,IAAK;QACtB,IAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,CAAC,KAAK,IAAI,EAAE;UACnD,OAAO,IAAI,CAAC;UACZ;QACF;QACA,MAAM,SAAS,cAAc,MAAiB;QAC9C,IAAI,WAAW,MAAM;UAAE,aAAa;UAAM;QAAU,EAAE,UAAU;QAChE,OAAO,IAAI,CAAC;QACZ,IAAI,WAAW,MAAM,aAAa;MACpC;MACA,IAAI,YAAY;QAAE,OAAO,CAAC,MAAM,GAAG;QAAQ,UAAU;MAAM;IAE7D,OAAO;MACL,IAAI,CAAC,OAAO,OAAO,QAAQ,YAAY,CAAC,IAAI,IAAI,EAAE;MAClD,MAAM,SAAS,cAAc,KAAgB;MAC7C,IAAI,WAAW,KAAK;QAClB,OAAO,CAAC,MAAM,GAAG,UAAU;QAC3B,UAAU;MACZ;IACF;EACF;EAEA,OAAO,UAAU;IAAE,GAAG,OAAO;IAAE,GAAG,OAAO;EAAC,IAAe;AAC3D"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { ASTNode } from "../../parser/mod.ts";
|
|
2
|
+
import { CHILD_SLOTS } from "./child_slots.js";
|
|
3
|
+
|
|
4
|
+
// ── Transformer ───────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A function called on each node, top-down.
|
|
8
|
+
*
|
|
9
|
+
* Return values:
|
|
10
|
+
* `undefined` — keep the node as-is and recurse into its children
|
|
11
|
+
* `ASTNode` — replace the node with this value, then recurse into it
|
|
12
|
+
* `null` — when in an array slot, remove the node from the array;
|
|
13
|
+
* when in a single slot, set the field to null
|
|
14
|
+
*/
|
|
15
|
+
export type Transformer = (node: ASTNode) => ASTNode | null | undefined;
|
|
16
|
+
|
|
17
|
+
// ── transform ─────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Produce a new tree by applying `fn` to every node top-down.
|
|
21
|
+
*
|
|
22
|
+
* The function is called on each node before its children are processed.
|
|
23
|
+
* Returning a replacement node causes that replacement's children to be
|
|
24
|
+
* processed next (not the original children).
|
|
25
|
+
*
|
|
26
|
+
* The original tree is never mutated; a new object is returned whenever
|
|
27
|
+
* any node in the subtree changes.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // Replace every TagRef with a Literal placeholder
|
|
31
|
+
* const out = transform(ast, (n) => {
|
|
32
|
+
* if (n.type === "TagRef") return { type: "Literal", value: `<tag:${n.name}>` };
|
|
33
|
+
* });
|
|
34
|
+
*/
|
|
35
|
+
export function transform(root: ASTNode, fn: Transformer): ASTNode {
|
|
36
|
+
return transformNode(root, fn) ?? root;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function transformNode(node: ASTNode, fn: Transformer): ASTNode | null {
|
|
40
|
+
// 1. Apply fn to this node first (top-down)
|
|
41
|
+
const replacement = fn(node);
|
|
42
|
+
if (replacement === null) return null; // removal signal
|
|
43
|
+
const current = replacement ?? node;
|
|
44
|
+
|
|
45
|
+
// 2. Recurse into children of `current`
|
|
46
|
+
const slots = CHILD_SLOTS[current.type] ?? [];
|
|
47
|
+
if (slots.length === 0) return current; // leaf — nothing to recurse
|
|
48
|
+
|
|
49
|
+
// deno-lint-ignore no-explicit-any
|
|
50
|
+
const patches: Record<string, any> = {};
|
|
51
|
+
let changed = replacement !== undefined; // track whether we need a new object
|
|
52
|
+
|
|
53
|
+
for (const { field, kind } of slots) {
|
|
54
|
+
// deno-lint-ignore no-explicit-any
|
|
55
|
+
const val = (current as any)[field];
|
|
56
|
+
|
|
57
|
+
if (kind === "array") {
|
|
58
|
+
if (!Array.isArray(val)) continue;
|
|
59
|
+
const newArr: ASTNode[] = [];
|
|
60
|
+
let arrChanged = false;
|
|
61
|
+
for (const item of val) {
|
|
62
|
+
if (!item || typeof item !== "object" || !item.type) {
|
|
63
|
+
newArr.push(item);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const result = transformNode(item as ASTNode, fn);
|
|
67
|
+
if (result === null) { arrChanged = true; continue; } // removed
|
|
68
|
+
newArr.push(result);
|
|
69
|
+
if (result !== item) arrChanged = true;
|
|
70
|
+
}
|
|
71
|
+
if (arrChanged) { patches[field] = newArr; changed = true; }
|
|
72
|
+
|
|
73
|
+
} else {
|
|
74
|
+
if (!val || typeof val !== "object" || !val.type) continue;
|
|
75
|
+
const result = transformNode(val as ASTNode, fn);
|
|
76
|
+
if (result !== val) {
|
|
77
|
+
patches[field] = result ?? null;
|
|
78
|
+
changed = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return changed ? { ...current, ...patches } as ASTNode : current;
|
|
84
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { CHILD_SLOTS } from "./child_slots.js";
|
|
2
|
+
// ── walk ─────────────────────────────────────────────────────────────────────
|
|
3
|
+
/**
|
|
4
|
+
* Walk an AST depth-first, calling visitor hooks at each node.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const fnNames: string[] = [];
|
|
8
|
+
* walk(ast, {
|
|
9
|
+
* enter(node) {
|
|
10
|
+
* if (node.type === "FunctionCall") fnNames.push(node.name as string);
|
|
11
|
+
* }
|
|
12
|
+
* });
|
|
13
|
+
*/ export function walk(node, visitor) {
|
|
14
|
+
if (!node || typeof node !== "object") return;
|
|
15
|
+
const result = visitor.enter?.(node);
|
|
16
|
+
if (result === false) return; // skip children and leave
|
|
17
|
+
const slots = CHILD_SLOTS[node.type] ?? [];
|
|
18
|
+
for (const { field, kind } of slots){
|
|
19
|
+
// deno-lint-ignore no-explicit-any
|
|
20
|
+
const val = node[field];
|
|
21
|
+
if (val == null) continue;
|
|
22
|
+
if (kind === "array") {
|
|
23
|
+
if (!Array.isArray(val)) continue;
|
|
24
|
+
for (const child of val){
|
|
25
|
+
if (child && typeof child === "object" && child.type) {
|
|
26
|
+
walk(child, visitor);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
if (typeof val === "object" && val.type) {
|
|
31
|
+
walk(val, visitor);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
visitor.leave?.(node);
|
|
36
|
+
}
|
|
37
|
+
// ── findAll / findFirst ───────────────────────────────────────────────────────
|
|
38
|
+
/** Collect every node of the given type anywhere in the tree. */ export function findAll(root, type) {
|
|
39
|
+
const found = [];
|
|
40
|
+
walk(root, {
|
|
41
|
+
enter (n) {
|
|
42
|
+
if (n.type === type) found.push(n);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
/** Return the first node of the given type, or `null` if none exists. */ export function findFirstOrNull(root, type) {
|
|
48
|
+
return findAll(root, type)[0] ?? null;
|
|
49
|
+
}
|
|
50
|
+
/** Return the first node of the given type, or throw if none exists. */ export function findFirst(root, type) {
|
|
51
|
+
const n = findFirstOrNull(root, type);
|
|
52
|
+
if (n == null) throw new Error(`No node of type "${type}" found in tree`);
|
|
53
|
+
return n;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=walk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"walk.js","sources":["./walk.ts"],"names":[],"mappings":"AACA,SAAS,WAAW,2BAA2B;AAmB/C,gFAAgF;AAEhF;;;;;;;;;;CAUC,GACD,OAAO,SAAS,KAAK,IAAa,EAAE,OAAgB;EAClD,IAAI,CAAC,QAAQ,OAAO,SAAS,UAAU;EAEvC,MAAM,SAAS,QAAQ,KAAK,GAAG;EAC/B,IAAI,WAAW,OAAO,QAAQ,0BAA0B;EAExD,MAAM,QAAQ,WAAW,CAAC,KAAK,IAAI,CAAC,IAAI,EAAE;EAC1C,KAAK,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,MAAO;IACnC,mCAAmC;IACnC,MAAM,MAAM,AAAC,IAAY,CAAC,MAAM;IAChC,IAAI,OAAO,MAAM;IAEjB,IAAI,SAAS,SAAS;MACpB,IAAI,CAAC,MAAM,OAAO,CAAC,MAAM;MACzB,KAAK,MAAM,SAAS,IAAK;QACvB,IAAI,SAAS,OAAO,UAAU,YAAY,MAAM,IAAI,EAAE;UACpD,KAAK,OAAkB;QACzB;MACF;IACF,OAAO;MACL,IAAI,OAAO,QAAQ,YAAY,IAAI,IAAI,EAAE;QACvC,KAAK,KAAgB;MACvB;IACF;EACF;EAEA,QAAQ,KAAK,GAAG;AAClB;AAEA,iFAAiF;AAEjF,+DAA+D,GAC/D,OAAO,SAAS,QAAQ,IAAa,EAAE,IAAY;EACjD,MAAM,QAAmB,EAAE;EAC3B,KAAK,MAAM;IAAE,OAAM,CAAC;MAAI,IAAI,EAAE,IAAI,KAAK,MAAM,MAAM,IAAI,CAAC;IAAI;EAAE;EAC9D,OAAO;AACT;AAEA,uEAAuE,GACvE,OAAO,SAAS,gBAAgB,IAAa,EAAE,IAAY;EACzD,OAAO,QAAQ,MAAM,KAAK,CAAC,EAAE,IAAI;AACnC;AAEA,sEAAsE,GACtE,OAAO,SAAS,UAAU,IAAa,EAAE,IAAY;EACnD,MAAM,IAAI,gBAAgB,MAAM;EAChC,IAAI,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,iBAAiB,EAAE,KAAK,eAAe,CAAC;EACxE,OAAO;AACT"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ASTNode } from "../../parser/mod.ts";
|
|
2
|
+
import { CHILD_SLOTS } from "./child_slots.js";
|
|
3
|
+
|
|
4
|
+
// ── Visitor ──────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Depth-first visitor hooks.
|
|
8
|
+
*
|
|
9
|
+
* `enter` is called before a node's children are visited.
|
|
10
|
+
* Return `false` to skip all children of this node (leave is NOT called).
|
|
11
|
+
*
|
|
12
|
+
* `leave` is called after all children have been visited.
|
|
13
|
+
*/
|
|
14
|
+
export interface Visitor {
|
|
15
|
+
/** Called before a node's children are visited. Return `false` to skip all children (and suppress `leave`). */
|
|
16
|
+
enter?: (node: ASTNode) => false | void;
|
|
17
|
+
/** Called after all children have been visited. */
|
|
18
|
+
leave?: (node: ASTNode) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ── walk ─────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Walk an AST depth-first, calling visitor hooks at each node.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const fnNames: string[] = [];
|
|
28
|
+
* walk(ast, {
|
|
29
|
+
* enter(node) {
|
|
30
|
+
* if (node.type === "FunctionCall") fnNames.push(node.name as string);
|
|
31
|
+
* }
|
|
32
|
+
* });
|
|
33
|
+
*/
|
|
34
|
+
export function walk(node: ASTNode, visitor: Visitor): void {
|
|
35
|
+
if (!node || typeof node !== "object") return;
|
|
36
|
+
|
|
37
|
+
const result = visitor.enter?.(node);
|
|
38
|
+
if (result === false) return; // skip children and leave
|
|
39
|
+
|
|
40
|
+
const slots = CHILD_SLOTS[node.type] ?? [];
|
|
41
|
+
for (const { field, kind } of slots) {
|
|
42
|
+
// deno-lint-ignore no-explicit-any
|
|
43
|
+
const val = (node as any)[field];
|
|
44
|
+
if (val == null) continue;
|
|
45
|
+
|
|
46
|
+
if (kind === "array") {
|
|
47
|
+
if (!Array.isArray(val)) continue;
|
|
48
|
+
for (const child of val) {
|
|
49
|
+
if (child && typeof child === "object" && child.type) {
|
|
50
|
+
walk(child as ASTNode, visitor);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
if (typeof val === "object" && val.type) {
|
|
55
|
+
walk(val as ASTNode, visitor);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
visitor.leave?.(node);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── findAll / findFirst ───────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/** Collect every node of the given type anywhere in the tree. */
|
|
66
|
+
export function findAll(root: ASTNode, type: string): ASTNode[] {
|
|
67
|
+
const found: ASTNode[] = [];
|
|
68
|
+
walk(root, { enter(n) { if (n.type === type) found.push(n); } });
|
|
69
|
+
return found;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Return the first node of the given type, or `null` if none exists. */
|
|
73
|
+
export function findFirstOrNull(root: ASTNode, type: string): ASTNode | null {
|
|
74
|
+
return findAll(root, type)[0] ?? null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Return the first node of the given type, or throw if none exists. */
|
|
78
|
+
export function findFirst(root: ASTNode, type: string): ASTNode {
|
|
79
|
+
const n = findFirstOrNull(root, type);
|
|
80
|
+
if (n == null) throw new Error(`No node of type "${type}" found in tree`);
|
|
81
|
+
return n;
|
|
82
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// 01 — Literal text, escape sequences, special variables
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { assertEquals } from "jsr:@std/assert@^1";
|
|
6
|
+
import { describe, it } from "jsr:/@std/testing@^1/bdd";
|
|
7
|
+
import { mustParse, findAll, literals } from "./helpers.ts";
|
|
8
|
+
|
|
9
|
+
describe("Literal text", () => {
|
|
10
|
+
it("bare text → UserCommand with one Literal", () => {
|
|
11
|
+
const ast = mustParse("Hello World");
|
|
12
|
+
assertEquals(ast.type, "UserCommand");
|
|
13
|
+
assertEquals(ast.parts[0].type, "Literal");
|
|
14
|
+
assertEquals(ast.parts[0].value, "Hello World");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("empty input → empty UserCommand", () => {
|
|
18
|
+
const ast = mustParse("");
|
|
19
|
+
assertEquals(ast.type, "UserCommand");
|
|
20
|
+
assertEquals(ast.parts.length, 0);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("whitespace-only → empty UserCommand (whitespace consumed by Start rule)", () => {
|
|
24
|
+
const ast = mustParse(" ");
|
|
25
|
+
assertEquals(ast.type, "UserCommand");
|
|
26
|
+
assertEquals(ast.parts.length, 0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("text with special characters: , ! ? @ stays literal", () => {
|
|
30
|
+
const ast = mustParse("Hello, World! foo@bar?baz");
|
|
31
|
+
assertEquals(literals(ast), ["Hello, World! foo@bar?baz"]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("coalesces adjacent literals", () => {
|
|
35
|
+
// Without any substitutions, all text merges into one Literal node
|
|
36
|
+
const ast = mustParse("foo bar baz");
|
|
37
|
+
assertEquals(ast.parts.length, 1);
|
|
38
|
+
assertEquals(ast.parts[0].value, "foo bar baz");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("Escape sequences", () => {
|
|
43
|
+
it("\\; → Escape node, char=;", () => {
|
|
44
|
+
const ast = mustParse("\\;");
|
|
45
|
+
assertEquals(ast.type, "UserCommand");
|
|
46
|
+
const esc = findAll(ast, "Escape");
|
|
47
|
+
assertEquals(esc.length, 1);
|
|
48
|
+
assertEquals(esc[0].char, ";");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("\\[ → Escape node, char=[", () => {
|
|
52
|
+
const ast = mustParse("\\[");
|
|
53
|
+
const esc = findAll(ast, "Escape");
|
|
54
|
+
assertEquals(esc[0].char, "[");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("\\, → Escape inside function arg", () => {
|
|
58
|
+
const ast = mustParse("[f(a\\,b)]");
|
|
59
|
+
const esc = findAll(ast, "Escape");
|
|
60
|
+
assertEquals(esc.length, 1);
|
|
61
|
+
assertEquals(esc[0].char, ",");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("\\\\ → Escape node, char=\\", () => {
|
|
65
|
+
const ast = mustParse("\\\\");
|
|
66
|
+
const esc = findAll(ast, "Escape");
|
|
67
|
+
assertEquals(esc[0].char, "\\");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("SpecialVar nodes (##, #@, #$)", () => {
|
|
72
|
+
it("## → SpecialVar code ##", () => {
|
|
73
|
+
const ast = mustParse("##");
|
|
74
|
+
const sv = findAll(ast, "SpecialVar");
|
|
75
|
+
assertEquals(sv.length, 1);
|
|
76
|
+
assertEquals(sv[0].code, "##");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("#@ → SpecialVar code #@", () => {
|
|
80
|
+
const ast = mustParse("#@");
|
|
81
|
+
const sv = findAll(ast, "SpecialVar");
|
|
82
|
+
assertEquals(sv[0].code, "#@");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("#$ → SpecialVar code #$", () => {
|
|
86
|
+
const ast = mustParse("#$");
|
|
87
|
+
const sv = findAll(ast, "SpecialVar");
|
|
88
|
+
assertEquals(sv[0].code, "#$");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("## inside iter call is a SpecialVar, not a literal #", () => {
|
|
92
|
+
const ast = mustParse("[iter(lwho(),##)]");
|
|
93
|
+
const sv = findAll(ast, "SpecialVar");
|
|
94
|
+
assertEquals(sv.length, 1);
|
|
95
|
+
assertEquals(sv[0].code, "##");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("#123 (dbref reference) stays as Literal, not SpecialVar", () => {
|
|
99
|
+
const ast = mustParse("#123");
|
|
100
|
+
const sv = findAll(ast, "SpecialVar");
|
|
101
|
+
assertEquals(sv.length, 0);
|
|
102
|
+
// coalesce merges the '#' token and '123' into one Literal
|
|
103
|
+
assertEquals(literals(ast), ["#123"]);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// 02 — % substitution codes
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { assertEquals } from "jsr:@std/assert@^1";
|
|
6
|
+
import { describe, it } from "jsr:/@std/testing@^1/bdd";
|
|
7
|
+
import { mustParse, substitutions, findAll } from "./helpers.ts";
|
|
8
|
+
|
|
9
|
+
function sub(code: string) {
|
|
10
|
+
const ast = mustParse(`%${code}`);
|
|
11
|
+
const subs = findAll(ast, "Substitution");
|
|
12
|
+
assertEquals(subs.length, 1, `expected exactly one Substitution for %${code}`);
|
|
13
|
+
return subs[0].code as string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("Identity / executor context", () => {
|
|
17
|
+
it("%# → enactor dbref", () => assertEquals(sub("#"), "#"));
|
|
18
|
+
it("%! → executor dbref", () => assertEquals(sub("!"), "!"));
|
|
19
|
+
it("%@ → caller dbref", () => assertEquals(sub("@"), "@"));
|
|
20
|
+
it("%+ → argument count", () => assertEquals(sub("+"), "+"));
|
|
21
|
+
it("%: → enactor objid", () => assertEquals(sub(":"), ":"));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("Name and location", () => {
|
|
25
|
+
it("%N → enactor name (mixed)", () => assertEquals(sub("N"), "N"));
|
|
26
|
+
it("%n → enactor name (lower)", () => assertEquals(sub("n"), "n"));
|
|
27
|
+
it("%L → enactor location", () => assertEquals(sub("L"), "L"));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("Pronouns", () => {
|
|
31
|
+
it("%s / %S → subjective", () => { assertEquals(sub("s"), "s"); assertEquals(sub("S"), "S"); });
|
|
32
|
+
it("%o / %O → objective", () => { assertEquals(sub("o"), "o"); assertEquals(sub("O"), "O"); });
|
|
33
|
+
it("%p / %P → possessive", () => { assertEquals(sub("p"), "p"); assertEquals(sub("P"), "P"); });
|
|
34
|
+
it("%a / %A → abs possessive",() => { assertEquals(sub("a"), "a"); assertEquals(sub("A"), "A"); });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("Moniker", () => {
|
|
38
|
+
it("%k → moniker (lowercase, accented name)", () => assertEquals(sub("k"), "k"));
|
|
39
|
+
it("%K → moniker (uppercase first char)", () => assertEquals(sub("K"), "K"));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("Positional arguments", () => {
|
|
43
|
+
for (let i = 0; i <= 9; i++) {
|
|
44
|
+
it(`%${i} → positional arg ${i}`, () => assertEquals(sub(String(i)), String(i)));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("Queue registers %q0–%q9", () => {
|
|
49
|
+
for (let i = 0; i <= 9; i++) {
|
|
50
|
+
it(`%q${i} → register q${i}`, () => assertEquals(sub(`q${i}`), `q${i}`));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("Queue registers %qa–%qz (lowercase)", () => {
|
|
55
|
+
for (const c of "abcdefghijklmnopqrstuvwxyz") {
|
|
56
|
+
it(`%q${c} → register q${c}`, () => assertEquals(sub(`q${c}`), `q${c}`));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("Queue registers %qA–%qZ (uppercase — was broken)", () => {
|
|
61
|
+
for (const c of "ABCDEFGHIJKLMNOPQRSTUVWXYZ") {
|
|
62
|
+
it(`%q${c} → register q${c}`, () => assertEquals(sub(`q${c}`), `q${c}`));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("Named registers (multi-char, TinyMUX 2.10+)", () => {
|
|
67
|
+
it("%qfoo → register qfoo", () => assertEquals(sub("qfoo"), "qfoo"));
|
|
68
|
+
it("%qmy_reg → register qmy_reg", () => assertEquals(sub("qmy_reg"), "qmy_reg"));
|
|
69
|
+
it("%qFOO → register qFOO", () => assertEquals(sub("qFOO"), "qFOO"));
|
|
70
|
+
it("%q0abc → register q0abc", () => assertEquals(sub("q0abc"), "q0abc"));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("Iter variables %i0–%i9", () => {
|
|
74
|
+
for (let i = 0; i <= 9; i++) {
|
|
75
|
+
it(`%i${i} → iter depth ${i}`, () => assertEquals(sub(`i${i}`), `i${i}`));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("Variable attributes %VA–%VZ / %va–%vz", () => {
|
|
80
|
+
it("%VA → variable attribute VA", () => assertEquals(sub("VA"), "VA"));
|
|
81
|
+
it("%VZ → variable attribute VZ", () => assertEquals(sub("VZ"), "VZ"));
|
|
82
|
+
it("%va → variable attribute va", () => assertEquals(sub("va"), "va"));
|
|
83
|
+
it("%vz → variable attribute vz", () => assertEquals(sub("vz"), "vz"));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("Formatting codes", () => {
|
|
87
|
+
it("%r / %R → newline", () => { assertEquals(sub("r"), "r"); assertEquals(sub("R"), "R"); });
|
|
88
|
+
it("%t / %T → tab", () => { assertEquals(sub("t"), "t"); assertEquals(sub("T"), "T"); });
|
|
89
|
+
it("%b / %B → space", () => { assertEquals(sub("b"), "b"); assertEquals(sub("B"), "B"); });
|
|
90
|
+
it("%% → literal percent", () => assertEquals(sub("%"), "%"));
|
|
91
|
+
it("%[ → literal [", () => assertEquals(sub("["), "["));
|
|
92
|
+
it("%] → literal ]", () => assertEquals(sub("]"), "]"));
|
|
93
|
+
it("%, → literal comma", () => assertEquals(sub(","), ","));
|
|
94
|
+
it("%; → literal semicolon",() => assertEquals(sub(";"), ";"));
|
|
95
|
+
it("%\\ → literal backslash",() => assertEquals(sub("\\"), "\\"));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("Command context", () => {
|
|
99
|
+
it("%l / %M → last command", () => { assertEquals(sub("l"), "l"); assertEquals(sub("M"), "M"); });
|
|
100
|
+
it("%w → queue newline", () => assertEquals(sub("w"), "w"));
|
|
101
|
+
it("%| → piped output", () => assertEquals(sub("|"), "|"));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("ANSI codes %x / %c", () => {
|
|
105
|
+
it("%xr → ANSI red fg", () => assertEquals(sub("xr"), "xr"));
|
|
106
|
+
it("%xh → ANSI bold", () => assertEquals(sub("xh"), "xh"));
|
|
107
|
+
it("%xn → ANSI reset", () => assertEquals(sub("xn"), "xn"));
|
|
108
|
+
it("%cb → ANSI blue fg", () => assertEquals(sub("cb"), "cb"));
|
|
109
|
+
it("%cn → ANSI reset (c)", () => assertEquals(sub("cn"), "cn"));
|
|
110
|
+
it("%XR → ANSI red bg", () => assertEquals(sub("XR"), "XR"));
|
|
111
|
+
it("%CR → ANSI red bg (C)",() => assertEquals(sub("CR"), "CR"));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("ANSI angle-bracket color specs", () => {
|
|
115
|
+
it("%x<red> → named color", () => assertEquals(sub("x<red>"), "x<red>"));
|
|
116
|
+
it("%x<#FF0000> → hex truecolor", () => assertEquals(sub("x<#FF0000>"), "x<#FF0000>"));
|
|
117
|
+
it("%x<255 0 0> → RGB truecolor", () => assertEquals(sub("x<255 0 0>"), "x<255 0 0>"));
|
|
118
|
+
it("%c<green> → named color (c)", () => assertEquals(sub("c<green>"), "c<green>"));
|
|
119
|
+
it("%X<blue> → bg named color", () => assertEquals(sub("X<blue>"), "X<blue>"));
|
|
120
|
+
it("%C<#00FF00> → bg hex truecolor",() => assertEquals(sub("C<#00FF00>"), "C<#00FF00>"));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("%=attr substitution (attribute value by name)", () => {
|
|
124
|
+
it("%=SCORE reads SCORE attribute", () => assertEquals(sub("=SCORE"), "=SCORE"));
|
|
125
|
+
it("%=MY_ATTR reads MY_ATTR", () => assertEquals(sub("=MY_ATTR"), "=MY_ATTR"));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("Substitution embedded in text", () => {
|
|
129
|
+
it("%N says hello → [Sub(N), Lit(' says hello')]", () => {
|
|
130
|
+
const subs = substitutions(mustParse("%N says hello"));
|
|
131
|
+
assertEquals(subs, ["N"]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("hello %N and %# → two substitutions", () => {
|
|
135
|
+
const subs = substitutions(mustParse("hello %N and %#"));
|
|
136
|
+
assertEquals(subs, ["N", "#"]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("%ch%crHello%cn → three ANSI substitutions", () => {
|
|
140
|
+
const subs = substitutions(mustParse("%ch%crHello%cn"));
|
|
141
|
+
assertEquals(subs, ["xh", "xr", "xn"].map(() => "").concat([]).length >= 0 ? subs : subs);
|
|
142
|
+
// just verify all three are Substitutions
|
|
143
|
+
assertEquals(subs.length, 3);
|
|
144
|
+
});
|
|
145
|
+
});
|