@enmaq/mdgl 0.2.0 → 0.3.0
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/README.md +71 -33
- package/UNICODE-LICENSE.txt +39 -0
- package/bin/mdgl-adapter.js +31 -0
- package/bin/mdgl.js +105 -0
- package/dist/adapter.d.ts +7 -0
- package/dist/adapter.js +231 -0
- package/dist/check.d.ts +48 -0
- package/dist/check.js +374 -0
- package/dist/eval.d.ts +53 -0
- package/dist/eval.js +496 -0
- package/dist/index.d.ts +14 -15
- package/dist/index.js +36 -98
- package/dist/parse.d.ts +117 -0
- package/dist/parse.js +424 -0
- package/dist/play.d.ts +44 -0
- package/dist/play.js +153 -0
- package/dist/unicode-tables.d.ts +7 -0
- package/dist/unicode-tables.js +15 -0
- package/dist/unicode.d.ts +12 -0
- package/dist/unicode.js +109 -0
- package/package.json +12 -4
package/dist/parse.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** One diagnostic: 1-based line, 1-based UTF-8 byte column. */
|
|
2
|
+
export interface Diag {
|
|
3
|
+
line: number;
|
|
4
|
+
col: number;
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
export type Coord = {
|
|
8
|
+
kind: 'num';
|
|
9
|
+
value: number;
|
|
10
|
+
percent: boolean;
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'anchor';
|
|
13
|
+
name: string;
|
|
14
|
+
offset: number;
|
|
15
|
+
};
|
|
16
|
+
export interface Point {
|
|
17
|
+
x: Coord;
|
|
18
|
+
y: Coord;
|
|
19
|
+
col: number;
|
|
20
|
+
}
|
|
21
|
+
export type Shape = {
|
|
22
|
+
kind: 'text';
|
|
23
|
+
text: string;
|
|
24
|
+
big: boolean;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'rect';
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
border?: string;
|
|
30
|
+
title?: string;
|
|
31
|
+
} | {
|
|
32
|
+
kind: 'circle';
|
|
33
|
+
radius: number;
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'cube';
|
|
36
|
+
size: number;
|
|
37
|
+
style?: string;
|
|
38
|
+
};
|
|
39
|
+
export type Value = {
|
|
40
|
+
kind: 'number';
|
|
41
|
+
value: number;
|
|
42
|
+
suffix: string;
|
|
43
|
+
raw: string;
|
|
44
|
+
} | {
|
|
45
|
+
kind: 'keyword';
|
|
46
|
+
value: string;
|
|
47
|
+
raw: string;
|
|
48
|
+
} | {
|
|
49
|
+
kind: 'string';
|
|
50
|
+
value: string;
|
|
51
|
+
raw: string;
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'point';
|
|
54
|
+
value: Point;
|
|
55
|
+
raw: string;
|
|
56
|
+
};
|
|
57
|
+
export interface Arg {
|
|
58
|
+
key: string;
|
|
59
|
+
value?: Value;
|
|
60
|
+
col: number;
|
|
61
|
+
}
|
|
62
|
+
export interface SceneStmt {
|
|
63
|
+
kind: 'scene';
|
|
64
|
+
line: number;
|
|
65
|
+
col: number;
|
|
66
|
+
attrs: Array<{
|
|
67
|
+
key: string;
|
|
68
|
+
value: string;
|
|
69
|
+
col: number;
|
|
70
|
+
}>;
|
|
71
|
+
}
|
|
72
|
+
export interface ActorStmt {
|
|
73
|
+
kind: 'actor';
|
|
74
|
+
line: number;
|
|
75
|
+
col: number;
|
|
76
|
+
name: string;
|
|
77
|
+
shape: Shape;
|
|
78
|
+
at: Point;
|
|
79
|
+
color?: string;
|
|
80
|
+
parent?: string;
|
|
81
|
+
z?: number;
|
|
82
|
+
anchor?: Point;
|
|
83
|
+
}
|
|
84
|
+
export interface ActionStmt {
|
|
85
|
+
kind: 'action';
|
|
86
|
+
line: number;
|
|
87
|
+
col: number;
|
|
88
|
+
timeNs: number;
|
|
89
|
+
actor: string;
|
|
90
|
+
verb: string;
|
|
91
|
+
args: Arg[];
|
|
92
|
+
}
|
|
93
|
+
export interface VersionStmt {
|
|
94
|
+
kind: 'version';
|
|
95
|
+
line: number;
|
|
96
|
+
col: number;
|
|
97
|
+
ok: boolean;
|
|
98
|
+
}
|
|
99
|
+
export interface UnknownStmt {
|
|
100
|
+
kind: 'unknown';
|
|
101
|
+
line: number;
|
|
102
|
+
col: number;
|
|
103
|
+
}
|
|
104
|
+
export type Stmt = SceneStmt | ActorStmt | ActionStmt | VersionStmt | UnknownStmt;
|
|
105
|
+
/** Parsed (unchecked) document. `syntax` is the first syntax error, if any; statements before it are complete. */
|
|
106
|
+
export interface Document {
|
|
107
|
+
statements: Stmt[];
|
|
108
|
+
syntax?: Diag;
|
|
109
|
+
inputError?: Diag;
|
|
110
|
+
}
|
|
111
|
+
export declare const MAX_INPUT: number;
|
|
112
|
+
export declare const MAX_LINE = 16384;
|
|
113
|
+
export declare const bytes: (s: string) => number;
|
|
114
|
+
/** Nanoseconds from a nonnegative decimal-seconds token (optional trailing s already removed). */
|
|
115
|
+
export declare function decimalToNs(tok: string): number;
|
|
116
|
+
/** Parse source into a statement list. Never throws for string input. */
|
|
117
|
+
export declare function parse(source: string): Document;
|
package/dist/parse.js
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// MDGL 0.1 lexical + syntactic parser. Builds a source-located model; statement
|
|
3
|
+
// order and semantic rules live in check.ts. Written from SPEC.md and the
|
|
4
|
+
// conformance corpus only.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.bytes = exports.MAX_LINE = exports.MAX_INPUT = void 0;
|
|
7
|
+
exports.decimalToNs = decimalToNs;
|
|
8
|
+
exports.parse = parse;
|
|
9
|
+
exports.MAX_INPUT = 256 * 1024;
|
|
10
|
+
exports.MAX_LINE = 16384;
|
|
11
|
+
const bytes = (s) => Buffer.byteLength(s, 'utf8');
|
|
12
|
+
exports.bytes = bytes;
|
|
13
|
+
class SyntaxErr {
|
|
14
|
+
constructor(d) {
|
|
15
|
+
this.d = d;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const SIMPLE_ESC = { a: '\x07', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t', v: '\v', '\\': '\\', '"': '"' };
|
|
19
|
+
const isDigit = (c) => c >= '0' && c <= '9';
|
|
20
|
+
const isIdStart = (c) => /[A-Za-z_]/.test(c);
|
|
21
|
+
const isIdChar = (c) => /[A-Za-z0-9_]/.test(c);
|
|
22
|
+
const hex = (s) => /^[0-9a-fA-F]+$/.test(s);
|
|
23
|
+
/** Nanoseconds from a nonnegative decimal-seconds token (optional trailing s already removed). */
|
|
24
|
+
function decimalToNs(tok) {
|
|
25
|
+
const [w, f = ''] = tok.split('.');
|
|
26
|
+
const frac = (f + '0000000000').slice(0, 10);
|
|
27
|
+
let ns = Number(w || '0') * 1e9 + Number(frac.slice(0, 9)) + (frac[9] >= '5' ? 1 : 0);
|
|
28
|
+
return ns;
|
|
29
|
+
}
|
|
30
|
+
class Cur {
|
|
31
|
+
constructor(s, line) {
|
|
32
|
+
this.s = s;
|
|
33
|
+
this.line = line;
|
|
34
|
+
this.i = 0;
|
|
35
|
+
}
|
|
36
|
+
col(at = this.i) { return (0, exports.bytes)(this.s.slice(0, at)) + 1; }
|
|
37
|
+
err(msg, at = this.i) { throw new SyntaxErr({ line: this.line, col: this.col(at), message: msg }); }
|
|
38
|
+
ws() { while (this.i < this.s.length && (this.s[this.i] === ' ' || this.s[this.i] === '\t'))
|
|
39
|
+
this.i++; }
|
|
40
|
+
eof() { return this.i >= this.s.length; }
|
|
41
|
+
peek() { return this.s[this.i] ?? ''; }
|
|
42
|
+
eat(c) { if (this.s.startsWith(c, this.i)) {
|
|
43
|
+
this.i += c.length;
|
|
44
|
+
return true;
|
|
45
|
+
} return false; }
|
|
46
|
+
expect(c) { this.ws(); if (!this.eat(c))
|
|
47
|
+
this.err(this.eof() ? `unexpected end of line, expected ${JSON.stringify(c)}` : `expected ${JSON.stringify(c)} but found ${JSON.stringify(this.peek())}`); }
|
|
48
|
+
ident(what = 'identifier') {
|
|
49
|
+
this.ws();
|
|
50
|
+
if (!isIdStart(this.peek()))
|
|
51
|
+
this.err(this.eof() ? `unexpected end of line, expected ${what}` : `unexpected character ${JSON.stringify(this.peek())}`);
|
|
52
|
+
const st = this.i;
|
|
53
|
+
while (isIdChar(this.peek()))
|
|
54
|
+
this.i++;
|
|
55
|
+
return this.s.slice(st, this.i);
|
|
56
|
+
}
|
|
57
|
+
/** unsigned decimal; returns raw text */
|
|
58
|
+
unsigned() {
|
|
59
|
+
const st = this.i;
|
|
60
|
+
let n = 0;
|
|
61
|
+
while (isDigit(this.peek())) {
|
|
62
|
+
this.i++;
|
|
63
|
+
n++;
|
|
64
|
+
}
|
|
65
|
+
if (this.peek() === '.') {
|
|
66
|
+
this.i++;
|
|
67
|
+
while (isDigit(this.peek())) {
|
|
68
|
+
this.i++;
|
|
69
|
+
n++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (n === 0)
|
|
73
|
+
this.err('expected number', st);
|
|
74
|
+
return this.s.slice(st, this.i);
|
|
75
|
+
}
|
|
76
|
+
number() {
|
|
77
|
+
this.ws();
|
|
78
|
+
const st = this.i;
|
|
79
|
+
let neg = false;
|
|
80
|
+
if (this.peek() === '+' || this.peek() === '-') {
|
|
81
|
+
neg = this.peek() === '-';
|
|
82
|
+
this.i++;
|
|
83
|
+
}
|
|
84
|
+
const u = this.unsigned();
|
|
85
|
+
if (this.peek() === 'e' || this.peek() === 'E')
|
|
86
|
+
this.err('exponents are not allowed', this.i);
|
|
87
|
+
void st;
|
|
88
|
+
return neg ? -Number(u) : Number(u);
|
|
89
|
+
}
|
|
90
|
+
string() {
|
|
91
|
+
this.ws();
|
|
92
|
+
const st = this.i;
|
|
93
|
+
if (!this.eat('"'))
|
|
94
|
+
this.err('expected string');
|
|
95
|
+
let out = '';
|
|
96
|
+
for (;;) {
|
|
97
|
+
if (this.eof())
|
|
98
|
+
this.err('unterminated string', st);
|
|
99
|
+
const c = this.s[this.i];
|
|
100
|
+
if (c === '"') {
|
|
101
|
+
this.i++;
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
if (c !== '\\') {
|
|
105
|
+
out += c;
|
|
106
|
+
this.i++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const eAt = this.i;
|
|
110
|
+
this.i++;
|
|
111
|
+
const e = this.s[this.i] ?? '';
|
|
112
|
+
if (e in SIMPLE_ESC) {
|
|
113
|
+
out += SIMPLE_ESC[e];
|
|
114
|
+
this.i++;
|
|
115
|
+
}
|
|
116
|
+
else if (e === 'x' || e === 'u' || e === 'U') {
|
|
117
|
+
const n = e === 'x' ? 2 : e === 'u' ? 4 : 8;
|
|
118
|
+
const h = this.s.slice(this.i + 1, this.i + 1 + n);
|
|
119
|
+
if (h.length !== n || !hex(h))
|
|
120
|
+
this.err('invalid escape in string', eAt);
|
|
121
|
+
const cp = parseInt(h, 16);
|
|
122
|
+
if (e !== 'x' && (cp > 0x10ffff || (cp >= 0xd800 && cp < 0xe000)))
|
|
123
|
+
this.err('invalid escape in string', eAt);
|
|
124
|
+
out += e === 'x' ? String.fromCharCode(cp) : String.fromCodePoint(cp);
|
|
125
|
+
this.i += 1 + n;
|
|
126
|
+
}
|
|
127
|
+
else if (e >= '0' && e <= '7') {
|
|
128
|
+
const o = this.s.slice(this.i, this.i + 3);
|
|
129
|
+
if (!/^[0-7]{3}$/.test(o) || parseInt(o, 8) > 255)
|
|
130
|
+
this.err('invalid escape in string', eAt);
|
|
131
|
+
out += String.fromCharCode(parseInt(o, 8));
|
|
132
|
+
this.i += 3;
|
|
133
|
+
}
|
|
134
|
+
else
|
|
135
|
+
this.err('invalid escape in string', eAt);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
coord() {
|
|
139
|
+
this.ws();
|
|
140
|
+
const c = this.peek();
|
|
141
|
+
if (isIdStart(c)) {
|
|
142
|
+
const at = this.i;
|
|
143
|
+
const name = this.ident();
|
|
144
|
+
if (!['center', 'left', 'right', 'top', 'bottom'].includes(name))
|
|
145
|
+
this.err(`unknown anchor ${JSON.stringify(name)}`, at);
|
|
146
|
+
let offset = 0;
|
|
147
|
+
this.ws();
|
|
148
|
+
if (this.peek() === '+' || this.peek() === '-') {
|
|
149
|
+
const neg = this.peek() === '-';
|
|
150
|
+
this.i++;
|
|
151
|
+
this.ws();
|
|
152
|
+
offset = Number(this.unsigned());
|
|
153
|
+
if (neg)
|
|
154
|
+
offset = -offset;
|
|
155
|
+
}
|
|
156
|
+
return { kind: 'anchor', name, offset };
|
|
157
|
+
}
|
|
158
|
+
const value = this.number();
|
|
159
|
+
const percent = this.eat('%');
|
|
160
|
+
return { kind: 'num', value, percent };
|
|
161
|
+
}
|
|
162
|
+
point() {
|
|
163
|
+
this.ws();
|
|
164
|
+
const col = this.col();
|
|
165
|
+
this.expect('(');
|
|
166
|
+
const x = this.coord();
|
|
167
|
+
this.expect(',');
|
|
168
|
+
const y = this.coord();
|
|
169
|
+
this.expect(')');
|
|
170
|
+
return { x, y, col };
|
|
171
|
+
}
|
|
172
|
+
value() {
|
|
173
|
+
this.ws();
|
|
174
|
+
const st = this.i;
|
|
175
|
+
const c = this.peek();
|
|
176
|
+
if (c === '(') {
|
|
177
|
+
const value = this.point();
|
|
178
|
+
return { kind: 'point', value, raw: this.s.slice(st, this.i) };
|
|
179
|
+
}
|
|
180
|
+
if (c === '"') {
|
|
181
|
+
const value = this.string();
|
|
182
|
+
return { kind: 'string', value, raw: this.s.slice(st, this.i) };
|
|
183
|
+
}
|
|
184
|
+
if (c === '+' || c === '-' || c === '.' || isDigit(c)) {
|
|
185
|
+
const value = this.number();
|
|
186
|
+
const sst = this.i;
|
|
187
|
+
while (/[A-Za-z]/.test(this.peek()))
|
|
188
|
+
this.i++;
|
|
189
|
+
return { kind: 'number', value, suffix: this.s.slice(sst, this.i), raw: this.s.slice(st, this.i) };
|
|
190
|
+
}
|
|
191
|
+
if (isIdStart(c)) {
|
|
192
|
+
this.ident();
|
|
193
|
+
while (this.peek() === '-' && isIdStart(this.s[this.i + 1] ?? '')) {
|
|
194
|
+
this.i++;
|
|
195
|
+
this.ident();
|
|
196
|
+
}
|
|
197
|
+
return { kind: 'keyword', value: this.s.slice(st, this.i), raw: this.s.slice(st, this.i) };
|
|
198
|
+
}
|
|
199
|
+
return this.err(this.eof() ? 'unexpected end of line, expected value' : `unexpected character ${JSON.stringify(c)}`);
|
|
200
|
+
}
|
|
201
|
+
end() { this.ws(); if (!this.eof())
|
|
202
|
+
this.err(`unexpected character ${JSON.stringify(this.peek())}`); }
|
|
203
|
+
}
|
|
204
|
+
function parseShape(c) {
|
|
205
|
+
const at = c.i;
|
|
206
|
+
const kind = c.ident('shape');
|
|
207
|
+
c.expect('(');
|
|
208
|
+
if (kind === 'text') {
|
|
209
|
+
const text = c.string();
|
|
210
|
+
let big = false;
|
|
211
|
+
c.ws();
|
|
212
|
+
if (c.eat(',')) {
|
|
213
|
+
const w = c.ident();
|
|
214
|
+
if (w !== 'big')
|
|
215
|
+
c.err(`unknown text option ${JSON.stringify(w)}`);
|
|
216
|
+
big = true;
|
|
217
|
+
}
|
|
218
|
+
c.expect(')');
|
|
219
|
+
return { kind, text, big };
|
|
220
|
+
}
|
|
221
|
+
if (kind === 'circle') {
|
|
222
|
+
const radius = c.number();
|
|
223
|
+
c.expect(')');
|
|
224
|
+
return { kind, radius };
|
|
225
|
+
}
|
|
226
|
+
if (kind === 'rect') {
|
|
227
|
+
const width = c.number();
|
|
228
|
+
c.expect(',');
|
|
229
|
+
const height = c.number();
|
|
230
|
+
const s = { kind, width, height };
|
|
231
|
+
const seen = new Set();
|
|
232
|
+
for (c.ws(); c.eat(','); c.ws()) {
|
|
233
|
+
const kat = c.i;
|
|
234
|
+
const k = c.ident();
|
|
235
|
+
if (k !== 'border' && k !== 'title')
|
|
236
|
+
c.err(`unknown rect argument ${JSON.stringify(k)}`, kat);
|
|
237
|
+
if (seen.has(k))
|
|
238
|
+
c.err(`duplicate rect argument ${k}`, kat);
|
|
239
|
+
seen.add(k);
|
|
240
|
+
c.expect('=');
|
|
241
|
+
if (k === 'title')
|
|
242
|
+
s.title = c.string();
|
|
243
|
+
else {
|
|
244
|
+
c.ws();
|
|
245
|
+
const v = c.ident();
|
|
246
|
+
if (v !== 'square' && v !== 'round')
|
|
247
|
+
c.err(`unknown border ${JSON.stringify(v)}`);
|
|
248
|
+
s.border = v;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
c.expect(')');
|
|
252
|
+
return s;
|
|
253
|
+
}
|
|
254
|
+
if (kind === 'cube') {
|
|
255
|
+
let size;
|
|
256
|
+
let style;
|
|
257
|
+
for (let first = true;; first = false) {
|
|
258
|
+
c.ws();
|
|
259
|
+
if (!first && !c.eat(','))
|
|
260
|
+
break;
|
|
261
|
+
const kat = c.i;
|
|
262
|
+
c.ws();
|
|
263
|
+
const k = c.ident();
|
|
264
|
+
c.expect('=');
|
|
265
|
+
if (k === 'size') {
|
|
266
|
+
if (size !== undefined)
|
|
267
|
+
c.err('duplicate cube argument size', kat);
|
|
268
|
+
size = c.number();
|
|
269
|
+
}
|
|
270
|
+
else if (k === 'style') {
|
|
271
|
+
if (style !== undefined)
|
|
272
|
+
c.err('duplicate cube argument style', kat);
|
|
273
|
+
c.ws();
|
|
274
|
+
style = c.ident();
|
|
275
|
+
if (style !== 'wire')
|
|
276
|
+
c.err('cube style must be wire');
|
|
277
|
+
}
|
|
278
|
+
else
|
|
279
|
+
c.err(`unknown cube argument ${JSON.stringify(k)}`, kat);
|
|
280
|
+
}
|
|
281
|
+
c.expect(')');
|
|
282
|
+
if (size === undefined)
|
|
283
|
+
c.err('cube requires size', at);
|
|
284
|
+
return { kind, size, style };
|
|
285
|
+
}
|
|
286
|
+
return c.err(`unknown shape ${JSON.stringify(kind)}`, at);
|
|
287
|
+
}
|
|
288
|
+
function parseActor(c, line, col) {
|
|
289
|
+
const name = c.ident('actor name');
|
|
290
|
+
c.expect('=');
|
|
291
|
+
c.ws();
|
|
292
|
+
const shape = parseShape(c);
|
|
293
|
+
c.ws();
|
|
294
|
+
const kw = c.i;
|
|
295
|
+
if (c.ident('"at"') !== 'at')
|
|
296
|
+
c.err('expected "at"', kw);
|
|
297
|
+
const at = c.point();
|
|
298
|
+
const a = { kind: 'actor', line, col, name, shape, at };
|
|
299
|
+
const seen = new Set();
|
|
300
|
+
for (c.ws(); !c.eof(); c.ws()) {
|
|
301
|
+
const kat = c.i;
|
|
302
|
+
const k = c.ident();
|
|
303
|
+
if (!['color', 'parent', 'z', 'anchor'].includes(k))
|
|
304
|
+
c.err(`unknown actor attribute ${JSON.stringify(k)}`, kat);
|
|
305
|
+
if (seen.has(k))
|
|
306
|
+
c.err(`duplicate actor attribute ${k}`, kat);
|
|
307
|
+
seen.add(k);
|
|
308
|
+
c.expect('=');
|
|
309
|
+
c.ws();
|
|
310
|
+
if (k === 'z')
|
|
311
|
+
a.z = c.number();
|
|
312
|
+
else if (k === 'anchor')
|
|
313
|
+
a.anchor = c.point();
|
|
314
|
+
else
|
|
315
|
+
a[k] = c.ident();
|
|
316
|
+
}
|
|
317
|
+
return a;
|
|
318
|
+
}
|
|
319
|
+
function parseAction(c, line, col) {
|
|
320
|
+
c.ws();
|
|
321
|
+
const tat = c.i;
|
|
322
|
+
const t0 = c.unsigned();
|
|
323
|
+
const tok = c.eat('s') ? t0 : t0;
|
|
324
|
+
const timeNs = decimalToNs(tok);
|
|
325
|
+
if (!(timeNs >= 0) || timeNs > 1e15)
|
|
326
|
+
c.err('time out of range', tat);
|
|
327
|
+
c.expect(':');
|
|
328
|
+
const actor = c.ident('actor name');
|
|
329
|
+
c.expect('.');
|
|
330
|
+
const verb = c.ident('action verb');
|
|
331
|
+
c.expect('(');
|
|
332
|
+
const args = [];
|
|
333
|
+
c.ws();
|
|
334
|
+
for (let first = true;; first = false) {
|
|
335
|
+
c.ws();
|
|
336
|
+
if (first && c.peek() === ')')
|
|
337
|
+
c.err('expected argument');
|
|
338
|
+
if (!first && !c.eat(','))
|
|
339
|
+
break;
|
|
340
|
+
c.ws();
|
|
341
|
+
const kcol = c.col();
|
|
342
|
+
const key = c.ident('argument');
|
|
343
|
+
c.ws();
|
|
344
|
+
if (c.eat('='))
|
|
345
|
+
args.push({ key, value: c.value(), col: kcol });
|
|
346
|
+
else
|
|
347
|
+
args.push({ key, col: kcol });
|
|
348
|
+
}
|
|
349
|
+
c.expect(')');
|
|
350
|
+
return { kind: 'action', line, col, timeNs, actor, verb, args };
|
|
351
|
+
}
|
|
352
|
+
/** Parse source into a statement list. Never throws for string input. */
|
|
353
|
+
function parse(source) {
|
|
354
|
+
if (typeof source !== 'string')
|
|
355
|
+
throw new TypeError('mdgl.parse: source must be a string');
|
|
356
|
+
const doc = { statements: [] };
|
|
357
|
+
if ((0, exports.bytes)(source) > exports.MAX_INPUT) {
|
|
358
|
+
doc.inputError = { line: 1, col: 1, message: 'input exceeds 256 KiB' };
|
|
359
|
+
return doc;
|
|
360
|
+
}
|
|
361
|
+
const lines = source.split('\n');
|
|
362
|
+
for (let i = 0; i < lines.length; i++) {
|
|
363
|
+
const n = i + 1;
|
|
364
|
+
if ((0, exports.bytes)(lines[i]) > exports.MAX_LINE) {
|
|
365
|
+
doc.inputError = { line: n, col: 1, message: 'line exceeds 16 KiB' };
|
|
366
|
+
return doc;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
for (let i = 0; i < lines.length; i++) {
|
|
370
|
+
const n = i + 1;
|
|
371
|
+
const raw = lines[i].endsWith('\r') ? lines[i].slice(0, -1) : lines[i];
|
|
372
|
+
const c = new Cur(raw, n);
|
|
373
|
+
c.ws();
|
|
374
|
+
if (c.eof() || c.peek() === '#')
|
|
375
|
+
continue;
|
|
376
|
+
const col = c.col();
|
|
377
|
+
try {
|
|
378
|
+
if (c.peek() === '@') {
|
|
379
|
+
c.i++;
|
|
380
|
+
doc.statements.push(parseAction(c, n, col));
|
|
381
|
+
c.end();
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const at = c.i;
|
|
385
|
+
const head = c.ident('statement');
|
|
386
|
+
void at;
|
|
387
|
+
if (head === 'mdgl') {
|
|
388
|
+
c.ws();
|
|
389
|
+
const ok = raw.slice(c.i).trim() === '0.1';
|
|
390
|
+
c.i = raw.length;
|
|
391
|
+
doc.statements.push({ kind: 'version', line: n, col, ok });
|
|
392
|
+
}
|
|
393
|
+
else if (head === 'scene') {
|
|
394
|
+
const attrs = [];
|
|
395
|
+
for (c.ws(); !c.eof(); c.ws()) {
|
|
396
|
+
const acol = c.col();
|
|
397
|
+
const key = c.ident('scene attribute');
|
|
398
|
+
c.expect('=');
|
|
399
|
+
const st = c.i;
|
|
400
|
+
while (!c.eof() && c.peek() !== ' ' && c.peek() !== '\t')
|
|
401
|
+
c.i++;
|
|
402
|
+
if (c.i === st)
|
|
403
|
+
c.err('expected scene attribute value');
|
|
404
|
+
attrs.push({ key, value: raw.slice(st, c.i), col: acol });
|
|
405
|
+
}
|
|
406
|
+
doc.statements.push({ kind: 'scene', line: n, col, attrs });
|
|
407
|
+
}
|
|
408
|
+
else if (head === 'actor') {
|
|
409
|
+
doc.statements.push(parseActor(c, n, col));
|
|
410
|
+
c.end();
|
|
411
|
+
}
|
|
412
|
+
else
|
|
413
|
+
doc.statements.push({ kind: 'unknown', line: n, col });
|
|
414
|
+
}
|
|
415
|
+
catch (e) {
|
|
416
|
+
if (e instanceof SyntaxErr) {
|
|
417
|
+
doc.syntax = e.d;
|
|
418
|
+
return doc;
|
|
419
|
+
}
|
|
420
|
+
throw e;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return doc;
|
|
424
|
+
}
|
package/dist/play.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Cell, Compiled } from './eval';
|
|
2
|
+
export interface Clock {
|
|
3
|
+
/** monotonic milliseconds */
|
|
4
|
+
now(): number;
|
|
5
|
+
setTimeout(fn: () => void, ms: number): unknown;
|
|
6
|
+
clearTimeout(handle: unknown): void;
|
|
7
|
+
}
|
|
8
|
+
export interface PlayDeps {
|
|
9
|
+
/** Write one chunk; may throw (e.g. EPIPE). One call is one atomic frame update. */
|
|
10
|
+
write(chunk: string): void;
|
|
11
|
+
clock: Clock;
|
|
12
|
+
/** Subscribe to asynchronous output errors; returns the unsubscribe function. */
|
|
13
|
+
onWriteError?(cb: (e: unknown) => void): () => void;
|
|
14
|
+
/** Subscribe to interrupts; cb receives the exit code (130 SIGINT, 143 SIGTERM). Returns unsubscribe. */
|
|
15
|
+
onInterrupt?(cb: (code: number) => void): () => void;
|
|
16
|
+
}
|
|
17
|
+
export type PlayEnd = 'done' | 'interrupt' | 'write-error' | 'stopped';
|
|
18
|
+
export interface PlayResult {
|
|
19
|
+
code: number;
|
|
20
|
+
reason: PlayEnd;
|
|
21
|
+
error?: unknown;
|
|
22
|
+
}
|
|
23
|
+
export interface PlayHandle {
|
|
24
|
+
done: Promise<PlayResult>;
|
|
25
|
+
stop(): void;
|
|
26
|
+
}
|
|
27
|
+
export declare const SYNC_BEGIN = "\u001B[?2026h";
|
|
28
|
+
export declare const SYNC_END = "\u001B[?2026l";
|
|
29
|
+
export declare const HIDE_CURSOR = "\u001B[?25l";
|
|
30
|
+
export declare const SHOW_CURSOR = "\u001B[?25h";
|
|
31
|
+
export declare const RESET = "\u001B[0m";
|
|
32
|
+
/** Never let a cell glyph act as a terminal escape: controls, DEL and C1 become a space. */
|
|
33
|
+
export declare function sanitizeRune(r: string): string;
|
|
34
|
+
/** Plain text of a frame (one row per line, trailing newline); no ANSI. */
|
|
35
|
+
export declare function plainText(cells: Cell[][]): string;
|
|
36
|
+
/** The fallback frame as plain text, for redirected output and reduced motion. */
|
|
37
|
+
export declare function plainFallback(scene: Compiled): string;
|
|
38
|
+
/**
|
|
39
|
+
* Start playback. Frame time is always computed from the monotonic clock (elapsed since start),
|
|
40
|
+
* never accumulated from tick counts, so late timers drop frames without changing state.
|
|
41
|
+
* Finite scenes stop when the evaluator reports done; every exit path clears the timer,
|
|
42
|
+
* unsubscribes listeners and restores the cursor and attributes exactly once.
|
|
43
|
+
*/
|
|
44
|
+
export declare function play(scene: Compiled, deps: PlayDeps): PlayHandle;
|