@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,781 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// MUX Softcode Grammar — PEG.js / Peggy
|
|
3
|
+
//
|
|
4
|
+
// Parses TinyMUX 2.x / PennMUSH softcode stored in attribute values.
|
|
5
|
+
// Produces a typed AST suitable for analysis, transformation, and linting.
|
|
6
|
+
//
|
|
7
|
+
// Allowed start rules: "Start" (attribute value), "LockExpr" (lock key)
|
|
8
|
+
//
|
|
9
|
+
// Install Peggy: npm install -g peggy
|
|
10
|
+
// Compile: peggy --allowed-start-rules Start,LockExpr mux-softcode.pegjs
|
|
11
|
+
//
|
|
12
|
+
// Quick test:
|
|
13
|
+
// const parser = require("./mux-softcode.js");
|
|
14
|
+
// parser.parse('$+finger *:@pemit %#=[u(me/FN_FINGER,%0)]');
|
|
15
|
+
//
|
|
16
|
+
// AST node types:
|
|
17
|
+
// AttributeValue DollarPattern ListenPattern PatternAlts Pattern Wildcard
|
|
18
|
+
// CommandList AtCommand AttributeSet UserCommand
|
|
19
|
+
// EvalBlock FunctionCall Arg
|
|
20
|
+
// BracedString Text
|
|
21
|
+
// Substitution SpecialVar Escape Literal
|
|
22
|
+
// LockOr LockAnd LockNot LockMe LockDbref
|
|
23
|
+
// LockFlagCheck LockTypeCheck LockAttrCheck LockPlayerName
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
{{
|
|
27
|
+
// ── Module-level helpers (shared across all parses) ──────────────────────
|
|
28
|
+
|
|
29
|
+
// Holds the `location` function injected by the per-parse initializer below.
|
|
30
|
+
// Safe because Peggy parsers are synchronous — no interleaving between parses.
|
|
31
|
+
let _loc = null;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Construct a typed AST node, attaching the current source location.
|
|
35
|
+
* `loc` is always present when called from a grammar action; it is absent
|
|
36
|
+
* only on nodes constructed manually in tests or by transform().
|
|
37
|
+
*/
|
|
38
|
+
function node(type, props) {
|
|
39
|
+
const n = Object.assign({ type }, props);
|
|
40
|
+
if (_loc) n.loc = _loc();
|
|
41
|
+
return n;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Merge adjacent Literal nodes to reduce AST noise.
|
|
46
|
+
* e.g. [Literal("foo"), Literal("bar")] → [Literal("foobar")]
|
|
47
|
+
* When merging, loc.end is extended to cover the full span.
|
|
48
|
+
*/
|
|
49
|
+
function coalesce(parts) {
|
|
50
|
+
if (!parts || parts.length === 0) return parts;
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const p of parts) {
|
|
53
|
+
if (
|
|
54
|
+
p.type === "Literal" &&
|
|
55
|
+
out.length > 0 &&
|
|
56
|
+
out[out.length - 1].type === "Literal"
|
|
57
|
+
) {
|
|
58
|
+
const last = out[out.length - 1];
|
|
59
|
+
last.value += p.value;
|
|
60
|
+
// Extend the span to cover the merged token
|
|
61
|
+
if (last.loc && p.loc) {
|
|
62
|
+
last.loc = { start: last.loc.start, end: p.loc.end };
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
out.push(p);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
}}
|
|
71
|
+
|
|
72
|
+
{
|
|
73
|
+
// Per-parse initializer: capture the `location` function for this parse run.
|
|
74
|
+
// Called once at the start of every peg$parse() invocation.
|
|
75
|
+
_loc = location;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// Entry Point
|
|
80
|
+
// ============================================================================
|
|
81
|
+
|
|
82
|
+
// Default start rule — parse a full attribute value.
|
|
83
|
+
// Leading/trailing whitespace is consumed so multi-line attribute values
|
|
84
|
+
// (whitespace-normalized when stored by MUX) parse cleanly.
|
|
85
|
+
Start
|
|
86
|
+
= _ av:AttributeValue _ { return av; }
|
|
87
|
+
|
|
88
|
+
// An attribute value is either a command-trigger definition or a command list.
|
|
89
|
+
AttributeValue
|
|
90
|
+
= DollarPattern
|
|
91
|
+
/ ListenPattern
|
|
92
|
+
/ CommandList
|
|
93
|
+
|
|
94
|
+
// Value of an @command or &attr-set after `=`.
|
|
95
|
+
// Like AttributeValue but uses CmdToken* for the plain case so that an
|
|
96
|
+
// unprotected semicolon at the outer level ends the command rather than
|
|
97
|
+
// being consumed into the value. Semicolons inside `{}` or `[]` are
|
|
98
|
+
// still protected and will not break out of the command.
|
|
99
|
+
AtCmdValue
|
|
100
|
+
= DollarPattern
|
|
101
|
+
/ ListenPattern
|
|
102
|
+
/ parts:CmdToken* {
|
|
103
|
+
return node("UserCommand", { parts: coalesce(parts) });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
// ============================================================================
|
|
108
|
+
// Dollar-Sign Pattern — $<pattern> : <action>
|
|
109
|
+
//
|
|
110
|
+
// Defines a soft-coded user command. The attribute value begins with `$`
|
|
111
|
+
// followed by a glob pattern, a `:`, and then a command action.
|
|
112
|
+
//
|
|
113
|
+
// Multiple pattern alternatives may be separated by `;` before the `:`.
|
|
114
|
+
//
|
|
115
|
+
// Examples:
|
|
116
|
+
// $+finger *:@pemit %#=[u(me/FN_FINGER,%0)]
|
|
117
|
+
// $hi;hello;hey *:@pemit %#=Greetings, %0!
|
|
118
|
+
// $+stat/set *=*:@switch [setq(0,pmatch(%0))]=1,{...},{...}
|
|
119
|
+
// ============================================================================
|
|
120
|
+
|
|
121
|
+
DollarPattern
|
|
122
|
+
= "$" pattern:PatternSpec ":" action:CommandList {
|
|
123
|
+
return node("DollarPattern", { pattern, action });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// Listen Pattern — ^<pattern> : <action>
|
|
128
|
+
//
|
|
129
|
+
// Defines a soft-coded listen trigger. When the object hears text that
|
|
130
|
+
// matches the pattern, the action is executed.
|
|
131
|
+
//
|
|
132
|
+
// Identical syntax to DollarPattern but triggered by ambient speech rather
|
|
133
|
+
// than typed commands.
|
|
134
|
+
//
|
|
135
|
+
// Examples:
|
|
136
|
+
// ^*hello*:@pemit %#=I heard you say hello!
|
|
137
|
+
// ^*help*;^*assist*:@pemit %#=Do you need help?
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
ListenPattern
|
|
141
|
+
= "^" pattern:PatternSpec ":" action:CommandList {
|
|
142
|
+
return node("ListenPattern", { pattern, action });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
// Multiple glob alternatives before the colon
|
|
147
|
+
PatternSpec
|
|
148
|
+
= head:SinglePattern tail:(";" SinglePattern)* {
|
|
149
|
+
const patterns = [head, ...tail.map(t => t[1])];
|
|
150
|
+
return patterns.length === 1
|
|
151
|
+
? patterns[0]
|
|
152
|
+
: node("PatternAlts", { patterns });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// One glob pattern — may contain * and ? wildcards and escape sequences
|
|
156
|
+
SinglePattern
|
|
157
|
+
= parts:PatternPiece+ {
|
|
158
|
+
return node("Pattern", { parts: coalesce(parts) });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
PatternPiece
|
|
162
|
+
= "*" { return node("Wildcard", { wildcard: "*" }); }
|
|
163
|
+
/ "?" { return node("Wildcard", { wildcard: "?" }); }
|
|
164
|
+
/ "\\" char:. { return node("Escape", { char }); }
|
|
165
|
+
/ chars:$([^;:*?\\]+) { return node("Literal", { value: chars }); }
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
// ============================================================================
|
|
169
|
+
// Command List — cmd ; cmd ; cmd ...
|
|
170
|
+
//
|
|
171
|
+
// Commands at the top level are separated by unprotected semicolons.
|
|
172
|
+
// A semicolon inside `{}` or `[]` does NOT end a command.
|
|
173
|
+
//
|
|
174
|
+
// When only one command is present, return it directly (no wrapping node).
|
|
175
|
+
// ============================================================================
|
|
176
|
+
|
|
177
|
+
CommandList
|
|
178
|
+
= head:Command tail:(";" Command)* {
|
|
179
|
+
const commands = [head, ...tail.map(t => t[1])];
|
|
180
|
+
return commands.length === 1
|
|
181
|
+
? commands[0]
|
|
182
|
+
: node("CommandList", { commands });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
// ============================================================================
|
|
187
|
+
// Command Forms
|
|
188
|
+
//
|
|
189
|
+
// MUX softcode has three kinds of commands at the top level:
|
|
190
|
+
// 1. @built-in commands: @pemit, @set, @dolist, @switch, @lock, …
|
|
191
|
+
// 2. Attribute-set commands: &ATTRNAME object=value
|
|
192
|
+
// 3. User/soft commands: +finger Bob, say Hello, go north, …
|
|
193
|
+
// ============================================================================
|
|
194
|
+
|
|
195
|
+
Command
|
|
196
|
+
= AtCommand
|
|
197
|
+
/ AttributeSet
|
|
198
|
+
/ UserCommand
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
// ── @command ──────────────────────────────────────────────────────────────
|
|
202
|
+
//
|
|
203
|
+
// @name[/switch]* [object[=value]]
|
|
204
|
+
//
|
|
205
|
+
// The value after `=` is parsed as a full AttributeValue, so it can itself
|
|
206
|
+
// contain dollar patterns (e.g., @trigger inside an attribute set) or nested
|
|
207
|
+
// command lists.
|
|
208
|
+
//
|
|
209
|
+
// Note: @command-specific argument syntax (e.g., @switch's comma-delimited
|
|
210
|
+
// cases, @wait's time:command form) is NOT parsed here — those cases appear
|
|
211
|
+
// as generic text inside the value. Semantic analysis is a separate concern.
|
|
212
|
+
//
|
|
213
|
+
// Examples:
|
|
214
|
+
// @pemit %#=Hello, [name(%#)]!
|
|
215
|
+
// @set me=SAFE
|
|
216
|
+
// @lock/enter me=flag^WIZARD
|
|
217
|
+
// @dolist [lwho()]={@pemit ##=Restart in 5 min.}
|
|
218
|
+
// @switch [gt(%0,10)]=1,{big},{small}
|
|
219
|
+
|
|
220
|
+
AtCommand
|
|
221
|
+
= "@" name:AtCmdName switches:AtSwitch* body:AtCmdBody? {
|
|
222
|
+
return node("AtCommand", {
|
|
223
|
+
name,
|
|
224
|
+
switches,
|
|
225
|
+
object: body ? body.object : null,
|
|
226
|
+
value: body ? body.value : null,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
AtCmdName = $([a-zA-Z][a-zA-Z0-9_-]*)
|
|
231
|
+
|
|
232
|
+
AtSwitch
|
|
233
|
+
= "/" n:$([a-zA-Z][a-zA-Z0-9_-]*) { return n; }
|
|
234
|
+
|
|
235
|
+
// The body of an @command: optional object, optional =value.
|
|
236
|
+
// Both alternatives begin with optional whitespace (_).
|
|
237
|
+
AtCmdBody
|
|
238
|
+
= _ obj:ObjText "=" val:AtCmdValue {
|
|
239
|
+
return { object: obj, value: val };
|
|
240
|
+
}
|
|
241
|
+
/ _ obj:ObjText {
|
|
242
|
+
return { object: obj, value: null };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
// ── Attribute-Set Command ──────────────────────────────────────────────────
|
|
247
|
+
//
|
|
248
|
+
// &ATTR_NAME object=value
|
|
249
|
+
//
|
|
250
|
+
// The value is a full AttributeValue, so it may be a DollarPattern
|
|
251
|
+
// (the common case when defining soft commands on objects).
|
|
252
|
+
//
|
|
253
|
+
// Examples:
|
|
254
|
+
// &DATA_SCORE me=100
|
|
255
|
+
// &FN_ADD me=[add(%0,%1)]
|
|
256
|
+
// &CMD_FINGER Global=$+finger *:@pemit %#=[u(me/FN_FINGER,%0)]
|
|
257
|
+
// &DATA_SCORE me ← no-value form clears the attribute
|
|
258
|
+
|
|
259
|
+
AttributeSet
|
|
260
|
+
= "&" attr:AttrIdent _ obj:ObjText "=" val:AtCmdValue {
|
|
261
|
+
return node("AttributeSet", { attribute: attr, object: obj, value: val, hidden: attr.startsWith("_") });
|
|
262
|
+
}
|
|
263
|
+
/ "&" attr:AttrIdent _ obj:ObjText {
|
|
264
|
+
return node("AttributeSet", { attribute: attr, object: obj, value: null, hidden: attr.startsWith("_") });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Attribute name: letters, digits, underscores, hyphens (case-sensitive in storage)
|
|
268
|
+
AttrIdent = $([a-zA-Z_][a-zA-Z0-9_-]*)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
// ── User / Soft Command (catch-all) ──────────────────────────────────────
|
|
272
|
+
//
|
|
273
|
+
// Anything that isn't an @command or &attr-set.
|
|
274
|
+
// Includes built-in player commands (say, go, look, …) and soft-coded
|
|
275
|
+
// user commands (+finger, +who, etc.) triggered from dollar patterns.
|
|
276
|
+
//
|
|
277
|
+
// Examples:
|
|
278
|
+
// +finger Bob
|
|
279
|
+
// say Hello, world!
|
|
280
|
+
// go north
|
|
281
|
+
|
|
282
|
+
UserCommand
|
|
283
|
+
= parts:CmdToken* {
|
|
284
|
+
return node("UserCommand", { parts: coalesce(parts) });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
// ============================================================================
|
|
289
|
+
// Object Text (before the `=` in a command)
|
|
290
|
+
//
|
|
291
|
+
// Used in both @command and &attr-set positions.
|
|
292
|
+
// Terminates at `=` or `;` (next command).
|
|
293
|
+
//
|
|
294
|
+
// Object names may contain spaces (e.g., "Finger Object"), dbrefs (#123),
|
|
295
|
+
// function results ([name(%#)]), and substitutions (%N).
|
|
296
|
+
// ============================================================================
|
|
297
|
+
|
|
298
|
+
ObjText
|
|
299
|
+
= parts:ObjToken+ {
|
|
300
|
+
return node("Text", { parts: coalesce(parts) });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
ObjToken
|
|
304
|
+
= EvalBlock
|
|
305
|
+
/ BracedString
|
|
306
|
+
/ Substitution
|
|
307
|
+
/ SpecialVar
|
|
308
|
+
/ TagRef
|
|
309
|
+
/ Escape
|
|
310
|
+
/ ObjLiteralChars
|
|
311
|
+
|
|
312
|
+
// Literal characters in object position: anything except = ; [ { % \ #
|
|
313
|
+
// Note: # excluded so ##/#@/#$ can always be picked up as SpecialVar
|
|
314
|
+
ObjLiteralChars
|
|
315
|
+
= chars:$([^=;\[{%\\#]+) { return node("Literal", { value: chars }); }
|
|
316
|
+
/ "#" !("#" / "@" / "$") { return node("Literal", { value: "#" }); }
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
// ============================================================================
|
|
320
|
+
// Command-Level Tokens
|
|
321
|
+
//
|
|
322
|
+
// Tokens that may appear inside a command's value/body.
|
|
323
|
+
// Terminates at `;` (next command).
|
|
324
|
+
// ============================================================================
|
|
325
|
+
|
|
326
|
+
CmdToken
|
|
327
|
+
= EvalBlock
|
|
328
|
+
/ BracedString
|
|
329
|
+
/ Substitution
|
|
330
|
+
/ SpecialVar
|
|
331
|
+
/ TagRef
|
|
332
|
+
/ Escape
|
|
333
|
+
/ CmdLiteralChars
|
|
334
|
+
|
|
335
|
+
// Literal characters at command level: anything except ; [ { % \ #
|
|
336
|
+
// # excluded so ##/#@/#$ are tried as SpecialVar before literal fallback.
|
|
337
|
+
CmdLiteralChars
|
|
338
|
+
= chars:$([^;\[{%\\#]+) { return node("Literal", { value: chars }); }
|
|
339
|
+
/ "#" !("#" / "@" / "$") { return node("Literal", { value: "#" }); }
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
// ============================================================================
|
|
343
|
+
// Braced String — { ... }
|
|
344
|
+
//
|
|
345
|
+
// Protects the contents from the surrounding parser:
|
|
346
|
+
// • Semicolons `;` inside braces do NOT separate commands.
|
|
347
|
+
// • Commas `,` inside braces do NOT separate function arguments.
|
|
348
|
+
// • Braces nest: { outer { inner } more }
|
|
349
|
+
//
|
|
350
|
+
// However, the following still apply inside braces:
|
|
351
|
+
// • %x substitutions (e.g., %N, %0, %q0)
|
|
352
|
+
// • [] evaluation (e.g., [add(1,2)])
|
|
353
|
+
// • \ escape sequences
|
|
354
|
+
//
|
|
355
|
+
// Examples:
|
|
356
|
+
// {don't;split;this}
|
|
357
|
+
// {@pemit %#=Hello, %0!} ← protects the semicolon
|
|
358
|
+
// {[add(%0,1)]} ← evaluation still happens
|
|
359
|
+
// ============================================================================
|
|
360
|
+
|
|
361
|
+
BracedString
|
|
362
|
+
= "{" parts:BracedToken* "}" {
|
|
363
|
+
return node("BracedString", { parts: coalesce(parts) });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
BracedToken
|
|
367
|
+
= BracedString // nested braces — braces always nest
|
|
368
|
+
/ EvalBlock // [] evaluation still applies inside {}
|
|
369
|
+
/ Substitution // %x substitution still applies
|
|
370
|
+
/ SpecialVar // ## #@ #$ still work
|
|
371
|
+
/ TagRef // #tagname still works inside braces
|
|
372
|
+
/ Escape // \ still escapes
|
|
373
|
+
/ BracedLiteralChars // everything else — including ; , = ( )
|
|
374
|
+
|
|
375
|
+
// Literal characters inside braces: anything except { } [ % \ #
|
|
376
|
+
// Note: ; and , are allowed here — that is the whole point of braces.
|
|
377
|
+
// # excluded so ##/#@/#$ can be picked up as SpecialVar.
|
|
378
|
+
BracedLiteralChars
|
|
379
|
+
= chars:$([^{}\[%\\#]+) { return node("Literal", { value: chars }); }
|
|
380
|
+
/ "#" !("#" / "@" / "$") { return node("Literal", { value: "#" }); }
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
// ============================================================================
|
|
384
|
+
// Eval Block — [ ... ]
|
|
385
|
+
//
|
|
386
|
+
// The content is evaluated and the result string replaces the block.
|
|
387
|
+
// Evaluation is innermost-first (deep-to-shallow nesting).
|
|
388
|
+
//
|
|
389
|
+
// The primary content is function calls, but substitutions, nested eval
|
|
390
|
+
// blocks, and literal text are also valid inside [].
|
|
391
|
+
//
|
|
392
|
+
// Examples:
|
|
393
|
+
// [add(1,2)] → "3"
|
|
394
|
+
// [name(%#)] → enactor's name
|
|
395
|
+
// [if(gt(%0,10),big,small)]
|
|
396
|
+
// [setq(0,pmatch(%0))][r(0)] → two back-to-back eval blocks
|
|
397
|
+
// [ansi(hg,SUCCESS)] → bold green "SUCCESS"
|
|
398
|
+
// ============================================================================
|
|
399
|
+
|
|
400
|
+
EvalBlock
|
|
401
|
+
= "[" parts:EvalToken* "]" {
|
|
402
|
+
return node("EvalBlock", { parts: coalesce(parts) });
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Inside an eval block, FunctionCall is tried first because it has a specific
|
|
406
|
+
// signature (identifier immediately followed by `(`). If that fails, fall
|
|
407
|
+
// through to the other token types.
|
|
408
|
+
EvalToken
|
|
409
|
+
= FunctionCall // name(arg, ...) — most common eval content
|
|
410
|
+
/ EvalBlock // nested []
|
|
411
|
+
/ BracedString // {} inside [] still protects content
|
|
412
|
+
/ Substitution // %x
|
|
413
|
+
/ SpecialVar // ## #@ #$
|
|
414
|
+
/ TagRef // #tagname
|
|
415
|
+
/ Escape // \x
|
|
416
|
+
/ EvalLiteralChars // anything except [ ] { % \
|
|
417
|
+
|
|
418
|
+
// In eval context, ( and ) can appear as literal characters
|
|
419
|
+
// (they are only syntactically meaningful after an identifier, handled by FunctionCall).
|
|
420
|
+
// # excluded so ##/#@/#$ are tried as SpecialVar before literal fallback.
|
|
421
|
+
EvalLiteralChars
|
|
422
|
+
= chars:$([^\[\]{}%\\#]+) { return node("Literal", { value: chars }); }
|
|
423
|
+
/ "#" !("#" / "@" / "$") { return node("Literal", { value: "#" }); }
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
// ============================================================================
|
|
427
|
+
// Function Call — name(arg, arg, ...)
|
|
428
|
+
//
|
|
429
|
+
// All MUX built-in and user-defined functions (via u()) follow this pattern.
|
|
430
|
+
// Function names are case-insensitive at runtime; this grammar preserves case.
|
|
431
|
+
//
|
|
432
|
+
// Zero-argument functions are supported: lwho(), rand(0), secs().
|
|
433
|
+
//
|
|
434
|
+
// Examples:
|
|
435
|
+
// add(1,2)
|
|
436
|
+
// if(gt(%0,10),big,small)
|
|
437
|
+
// u(me/FN_HELLO,%0,%1)
|
|
438
|
+
// iter([lcon(%L)],##: [get(%q0/S_##)], ,%b )
|
|
439
|
+
// setq(0,pmatch(trim(%0)))
|
|
440
|
+
// ============================================================================
|
|
441
|
+
|
|
442
|
+
// Zero-arg functions use the first alternative to avoid a zero-length match
|
|
443
|
+
// ambiguity: lwho() secs() time() rand() → args: []
|
|
444
|
+
// Functions with arguments use the second alternative.
|
|
445
|
+
FunctionCall
|
|
446
|
+
= name:FuncIdent "()" {
|
|
447
|
+
return node("FunctionCall", { name, args: [] });
|
|
448
|
+
}
|
|
449
|
+
/ name:FuncIdent "(" args:ArgList ")" {
|
|
450
|
+
return node("FunctionCall", { name, args });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Function identifiers: letters, digits, underscores (must start with letter/underscore)
|
|
454
|
+
FuncIdent = $([a-zA-Z_][a-zA-Z0-9_]*)
|
|
455
|
+
|
|
456
|
+
// Argument list: one or more arguments separated by commas.
|
|
457
|
+
// Empty positional args are valid: setq(0,) · iter(list,,delim)
|
|
458
|
+
// FuncArg accepts zero tokens so empty positions parse correctly.
|
|
459
|
+
ArgList
|
|
460
|
+
= head:FuncArg tail:("," FuncArg)* {
|
|
461
|
+
return [head, ...tail.map(t => t[1])];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// A single function argument — zero or more arg tokens.
|
|
465
|
+
// An empty arg (between two commas, or before/after the only comma) is valid.
|
|
466
|
+
FuncArg
|
|
467
|
+
= parts:ArgToken* {
|
|
468
|
+
return node("Arg", { parts: coalesce(parts) });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Tokens inside a function argument:
|
|
472
|
+
// • `,` and `)` terminate the argument — except when inside {} or []
|
|
473
|
+
// • {} braces protect commas: {a,b} passes literal "a,b" as one argument
|
|
474
|
+
// • [] blocks are evaluated: [add(1,2)] → "3" as part of the argument value
|
|
475
|
+
// • A bare `(` not following an identifier is a literal character (TinyMUX
|
|
476
|
+
// paren-stack semantics). The matching `)` is also consumed as literal
|
|
477
|
+
// via BalancedParens so common patterns like (text) and (a)(b) work.
|
|
478
|
+
// Note: commas inside BalancedParens still split arguments — use {} if
|
|
479
|
+
// you need a literal comma inside parentheses.
|
|
480
|
+
ArgToken
|
|
481
|
+
= FunctionCall // nested call: iter([lcon(%L)],name(##))
|
|
482
|
+
/ EvalBlock // [...] within an argument
|
|
483
|
+
/ BracedString // {...} — commas and ) inside are literal
|
|
484
|
+
/ BalancedParens // (literal text) — bare ( not preceded by identifier
|
|
485
|
+
/ Substitution // %x
|
|
486
|
+
/ SpecialVar // ## #@ #$
|
|
487
|
+
/ TagRef // #tagname
|
|
488
|
+
/ Escape // \x
|
|
489
|
+
/ ArgLiteralChars // everything except , ( ) [ ] { } % \
|
|
490
|
+
|
|
491
|
+
// Balanced parentheses in argument context.
|
|
492
|
+
// A bare `(` (not preceded by an identifier — which would be FunctionCall)
|
|
493
|
+
// is treated as a literal character. Its matching `)` is also literal.
|
|
494
|
+
// Commas inside are NOT protected — they still separate function arguments.
|
|
495
|
+
// For commas-inside-parens use braces: {(a,b)} passes "(a,b)" as one arg.
|
|
496
|
+
BalancedParens
|
|
497
|
+
= "(" parts:BalancedParenToken* ")" {
|
|
498
|
+
const inner = coalesce(parts);
|
|
499
|
+
return node("Literal", {
|
|
500
|
+
value: "(" + inner.map(p => p.type === "Literal" ? p.value : "").join("") + ")"
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Tokens inside BalancedParens — same as ArgToken except commas are allowed
|
|
505
|
+
// as literals (we're inside a paren group, not at a function arg boundary).
|
|
506
|
+
BalancedParenToken
|
|
507
|
+
= FunctionCall
|
|
508
|
+
/ EvalBlock
|
|
509
|
+
/ BracedString
|
|
510
|
+
/ BalancedParens
|
|
511
|
+
/ Substitution
|
|
512
|
+
/ SpecialVar
|
|
513
|
+
/ TagRef
|
|
514
|
+
/ Escape
|
|
515
|
+
/ BalancedParenLiteral
|
|
516
|
+
|
|
517
|
+
BalancedParenLiteral
|
|
518
|
+
= chars:$([^\[\](){}%\\#]+) { return node("Literal", { value: chars }); }
|
|
519
|
+
/ "#" !("#" / "@" / "$") { return node("Literal", { value: "#" }); }
|
|
520
|
+
|
|
521
|
+
// Literal characters inside function arguments.
|
|
522
|
+
// NOTE: ( and ) are excluded because ) ends the argument list and
|
|
523
|
+
// ( is handled by BalancedParens / FunctionCall above.
|
|
524
|
+
// Use {(text)} or \( to pass literal parens without the balancing rule.
|
|
525
|
+
// # excluded so ##/#@/#$ are tried as SpecialVar before literal fallback.
|
|
526
|
+
ArgLiteralChars
|
|
527
|
+
= chars:$([^,\[\](){}%\\#]+) { return node("Literal", { value: chars }); }
|
|
528
|
+
/ "#" !("#" / "@" / "$") { return node("Literal", { value: "#" }); }
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
// ============================================================================
|
|
532
|
+
// Substitutions — % + code
|
|
533
|
+
//
|
|
534
|
+
// Expanded at evaluation time to their runtime values.
|
|
535
|
+
//
|
|
536
|
+
// Identity / executor context:
|
|
537
|
+
// %# enactor's dbref %! executor's dbref (object running attr)
|
|
538
|
+
// %@ caller's dbref %+ number of positional arguments
|
|
539
|
+
//
|
|
540
|
+
// Names:
|
|
541
|
+
// %N enactor's name (mixed case) %n enactor's name (lowercase)
|
|
542
|
+
// %L enactor's location dbref
|
|
543
|
+
//
|
|
544
|
+
// Pronouns (resolved from enactor's SEX attribute):
|
|
545
|
+
// %s / %S subjective he/she/it He/She/It
|
|
546
|
+
// %o / %O objective him/her/it Him/Her/It
|
|
547
|
+
// %p / %P possessive his/her/its His/Her/Its
|
|
548
|
+
// %a / %A absolute his/hers/its His/Hers/Its
|
|
549
|
+
//
|
|
550
|
+
// Positional args:
|
|
551
|
+
// %0–%9 positional arguments (passed via u() or @trigger)
|
|
552
|
+
//
|
|
553
|
+
// Registers:
|
|
554
|
+
// %q0–%q9 local registers (set with setq())
|
|
555
|
+
// %qa–%qz extended registers (TinyMUX 2.10+)
|
|
556
|
+
//
|
|
557
|
+
// Iter / loop:
|
|
558
|
+
// %i0–%i9 nested iter() item at depth N (equivalent to itext(N))
|
|
559
|
+
//
|
|
560
|
+
// Variable attributes (VA–VZ on executor):
|
|
561
|
+
// %VA–%VZ value of attribute VA through VZ on the executor
|
|
562
|
+
// %va–%vz same, lowercase key variant (TinyMUX accepts both)
|
|
563
|
+
//
|
|
564
|
+
// Formatting:
|
|
565
|
+
// %r / %R carriage return / newline
|
|
566
|
+
// %t / %T tab character
|
|
567
|
+
// %b / %B space character
|
|
568
|
+
// %% literal percent sign
|
|
569
|
+
// %\ literal backslash
|
|
570
|
+
// %[ literal [
|
|
571
|
+
// %] literal ]
|
|
572
|
+
// %, literal comma
|
|
573
|
+
// %; literal semicolon
|
|
574
|
+
//
|
|
575
|
+
// Command context:
|
|
576
|
+
// %l / %M text of the last command entered
|
|
577
|
+
// %w newline if command came from queue, else empty
|
|
578
|
+
// %| output of previous piped command
|
|
579
|
+
//
|
|
580
|
+
// ANSI color / formatting (always followed by a code or <spec>):
|
|
581
|
+
// %xN / %cN single-letter ANSI code (e.g. %xr = red fg, %xh = bold)
|
|
582
|
+
// %x<spec> color spec: name, #RRGGBB, or R G B
|
|
583
|
+
// %XN / %CN uppercase ANSI variant
|
|
584
|
+
// %X<spec> / %C<spec>
|
|
585
|
+
// ============================================================================
|
|
586
|
+
|
|
587
|
+
Substitution
|
|
588
|
+
= "%" code:SubCode {
|
|
589
|
+
return node("Substitution", { code });
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
SubCode
|
|
593
|
+
// Registers (must come before single-char to avoid e.g. 'q' matching alone)
|
|
594
|
+
// TinyMUX supports %q0-%q9 (indices 0-9), %qa-%qz/%qA-%qZ (indices 10-35),
|
|
595
|
+
// and named registers (%qFoo, %qmy_reg — alphanumeric+underscore, max 32 chars).
|
|
596
|
+
= "q" name:$([0-9a-zA-Z_]+) { return "q" + name; }
|
|
597
|
+
/ "i" n:[0-9] { return "i" + n; }
|
|
598
|
+
|
|
599
|
+
// Variable attributes %VA–%VZ, %va–%vz
|
|
600
|
+
/ "V" n:[A-Za-z] { return "V" + n; }
|
|
601
|
+
/ "v" n:[A-Za-z] { return "v" + n; }
|
|
602
|
+
|
|
603
|
+
// ANSI color with angle-bracket spec (try before single-letter form)
|
|
604
|
+
/ "x" "<" s:$([^>]*) ">" { return "x<" + s + ">"; }
|
|
605
|
+
/ "X" "<" s:$([^>]*) ">" { return "X<" + s + ">"; }
|
|
606
|
+
/ "c" "<" s:$([^>]*) ">" { return "c<" + s + ">"; }
|
|
607
|
+
/ "C" "<" s:$([^>]*) ">" { return "C<" + s + ">"; }
|
|
608
|
+
|
|
609
|
+
// ANSI color single-letter %xr, %xh, %cb, etc.
|
|
610
|
+
/ "x" n:[a-zA-Z0-9] { return "x" + n; }
|
|
611
|
+
/ "X" n:[a-zA-Z0-9] { return "X" + n; }
|
|
612
|
+
/ "c" n:[a-zA-Z0-9] { return "c" + n; }
|
|
613
|
+
/ "C" n:[a-zA-Z0-9] { return "C" + n; }
|
|
614
|
+
|
|
615
|
+
// Literal backslash
|
|
616
|
+
/ "\\" { return "\\"; }
|
|
617
|
+
|
|
618
|
+
// %=ATTR — read an attribute value by name on the executor (TinyMUX 2.x)
|
|
619
|
+
// e.g. %=SCORE reads the SCORE attribute. Must come before the bare = case.
|
|
620
|
+
/ "=" name:$([a-zA-Z_][a-zA-Z0-9_-]*) { return "=" + name; }
|
|
621
|
+
|
|
622
|
+
// All remaining single-character codes
|
|
623
|
+
// %k/%K = moniker (accented name)
|
|
624
|
+
// %: = enactor object-id (#dbref:creation_timestamp)
|
|
625
|
+
/ c:[#NnSOPAsopaL!0-9RBTMrtblw@+|%kK:] { return c; }
|
|
626
|
+
|
|
627
|
+
// Literal bracket / punctuation codes
|
|
628
|
+
/ "[" { return "["; }
|
|
629
|
+
/ "]" { return "]"; }
|
|
630
|
+
/ "," { return ","; }
|
|
631
|
+
/ ";" { return ";"; }
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
// ============================================================================
|
|
635
|
+
// Special Variables — ## · #@ · #$
|
|
636
|
+
//
|
|
637
|
+
// Used inside iter() and @dolist to reference the current iteration state.
|
|
638
|
+
//
|
|
639
|
+
// ## current list item value (= itext(0) at the innermost level)
|
|
640
|
+
// #@ current list item position (1-indexed; = inum(0))
|
|
641
|
+
// #$ last dbref returned by a name-lookup function
|
|
642
|
+
//
|
|
643
|
+
// These are tried as higher-priority alternatives before literal text in every
|
|
644
|
+
// token context, so they are always recognised even when adjacent to other #
|
|
645
|
+
// characters (e.g., #1 is still a literal dbref reference).
|
|
646
|
+
// ============================================================================
|
|
647
|
+
|
|
648
|
+
SpecialVar
|
|
649
|
+
= "##" { return node("SpecialVar", { code: "##" }); }
|
|
650
|
+
/ "#@" { return node("SpecialVar", { code: "#@" }); }
|
|
651
|
+
/ "#$" { return node("SpecialVar", { code: "#$" }); }
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
// ============================================================================
|
|
655
|
+
// Tag Reference — #tagname (RhostMUSH)
|
|
656
|
+
//
|
|
657
|
+
// RhostMUSH supports a tag system (@tag/@ltag) where objects can be assigned
|
|
658
|
+
// named tags. `#tagname` is a shorthand that resolves to the object's dbref
|
|
659
|
+
// at runtime via objecttag_get().
|
|
660
|
+
//
|
|
661
|
+
// Tag names begin with a letter or underscore and may contain letters, digits,
|
|
662
|
+
// underscores, and hyphens. Numeric dbrefs (#123) are NOT TagRefs — they are
|
|
663
|
+
// handled by the literal fallback rules. ##, #@, #$ are SpecialVars and take
|
|
664
|
+
// priority because SpecialVar is tried before TagRef in every token list.
|
|
665
|
+
//
|
|
666
|
+
// Examples:
|
|
667
|
+
// #weather → TagRef(name="weather")
|
|
668
|
+
// #_localdb → TagRef(name="_localdb")
|
|
669
|
+
// #my-tag → TagRef(name="my-tag")
|
|
670
|
+
// ============================================================================
|
|
671
|
+
|
|
672
|
+
TagRef
|
|
673
|
+
= "#" name:$([a-zA-Z_][a-zA-Z0-9_-]*) {
|
|
674
|
+
return node("TagRef", { name });
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
// ============================================================================
|
|
679
|
+
// Escape Sequence — \ + char
|
|
680
|
+
//
|
|
681
|
+
// Prevents one level of evaluation for the next character.
|
|
682
|
+
// In command context: `;` → literal semicolon, `[` → literal bracket, etc.
|
|
683
|
+
// In function-arg context: `,` → literal comma, `)` → literal close-paren.
|
|
684
|
+
//
|
|
685
|
+
// The grammar records the escaped character as-is for later analysis.
|
|
686
|
+
// ============================================================================
|
|
687
|
+
|
|
688
|
+
Escape
|
|
689
|
+
= "\\" char:. {
|
|
690
|
+
return node("Escape", { char });
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
// ============================================================================
|
|
695
|
+
// Lock Expression Grammar
|
|
696
|
+
//
|
|
697
|
+
// Lock expressions are used as values in @lock commands.
|
|
698
|
+
// This grammar can be used as an alternate start rule for parsing lock keys.
|
|
699
|
+
//
|
|
700
|
+
// Example lock expressions:
|
|
701
|
+
// me owner only
|
|
702
|
+
// #123 specific dbref
|
|
703
|
+
// flag^WIZARD players with WIZARD flag
|
|
704
|
+
// !me anyone except owner
|
|
705
|
+
// me|#123 owner OR dbref #123
|
|
706
|
+
// meLj owner AND #456
|
|
707
|
+
// =PlayerName specific player by name
|
|
708
|
+
// type^ROOM type check
|
|
709
|
+
// flag^WIZARD|flag^ADMIN wizard or admin
|
|
710
|
+
//
|
|
711
|
+
// Operator precedence (lowest to highest):
|
|
712
|
+
// | OR
|
|
713
|
+
// & AND
|
|
714
|
+
// ! NOT (prefix)
|
|
715
|
+
// (primary terms)
|
|
716
|
+
// ============================================================================
|
|
717
|
+
|
|
718
|
+
LockExpr = LockOr
|
|
719
|
+
|
|
720
|
+
LockOr
|
|
721
|
+
= head:LockAnd tail:("|" LockAnd)* {
|
|
722
|
+
if (tail.length === 0) return head;
|
|
723
|
+
return node("LockOr", { operands: [head, ...tail.map(t => t[1])] });
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
LockAnd
|
|
727
|
+
= head:LockNot tail:("&" LockNot)* {
|
|
728
|
+
if (tail.length === 0) return head;
|
|
729
|
+
return node("LockAnd", { operands: [head, ...tail.map(t => t[1])] });
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
LockNot
|
|
733
|
+
= "!" operand:LockNot { return node("LockNot", { operand }); }
|
|
734
|
+
/ LockPrimary
|
|
735
|
+
|
|
736
|
+
LockPrimary
|
|
737
|
+
= "(" _ expr:LockExpr _ ")" { return expr; }
|
|
738
|
+
/ "me" ![a-zA-Z0-9_] { return node("LockMe", {}); }
|
|
739
|
+
/ LockDbref
|
|
740
|
+
/ LockFlagCheck
|
|
741
|
+
/ LockTypeCheck
|
|
742
|
+
/ LockAttrCheck
|
|
743
|
+
/ LockPlayerName
|
|
744
|
+
|
|
745
|
+
// #123 — specific object by dbref (#-1 is also valid in some contexts)
|
|
746
|
+
LockDbref
|
|
747
|
+
= "#" n:$("-"? [0-9]+) {
|
|
748
|
+
return node("LockDbref", { dbref: "#" + n });
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// flag^FLAGNAME — object must have this flag
|
|
752
|
+
LockFlagCheck
|
|
753
|
+
= "flag^" name:$([a-zA-Z_]+) {
|
|
754
|
+
return node("LockFlagCheck", { flag: name });
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// type^ROOM|THING|EXIT|PLAYER — object must be this type
|
|
758
|
+
LockTypeCheck
|
|
759
|
+
= "type^" name:$([a-zA-Z_]+) {
|
|
760
|
+
return node("LockTypeCheck", { typeName: name });
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// attr^ATTRNAME — object must have this attribute set
|
|
764
|
+
LockAttrCheck
|
|
765
|
+
= "attr^" name:$([a-zA-Z_][a-zA-Z0-9_-]*) {
|
|
766
|
+
return node("LockAttrCheck", { attribute: name });
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// =PlayerName — specific connected player by name
|
|
770
|
+
LockPlayerName
|
|
771
|
+
= "=" name:$([^|&!()[\]{}\r\n]+) {
|
|
772
|
+
return node("LockPlayerName", { name: name.trim() });
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
// ============================================================================
|
|
777
|
+
// Whitespace
|
|
778
|
+
// ============================================================================
|
|
779
|
+
|
|
780
|
+
_ = [ \t\r\n]*
|
|
781
|
+
__ = [ \t\r\n]+
|