@triflare/fx 0.0.0-stage → 1.0.1

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.
Files changed (39) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +73 -2
  3. package/bin/fx.js +1166 -0
  4. package/package.json +64 -4
  5. package/product.yml +28 -0
  6. package/src/brand.js +105 -0
  7. package/src/build.js +181 -0
  8. package/src/catalog.js +753 -0
  9. package/src/compiler.js +1658 -0
  10. package/src/config-edit.js +338 -0
  11. package/src/config.js +400 -0
  12. package/src/decompiler.js +551 -0
  13. package/src/dev.js +390 -0
  14. package/src/errors.js +56 -0
  15. package/src/extensions.js +747 -0
  16. package/src/index.js +35 -0
  17. package/src/language.js +12 -0
  18. package/src/lexer.js +202 -0
  19. package/src/log.js +70 -0
  20. package/src/package.js +202 -0
  21. package/src/packager.js +177 -0
  22. package/src/parser.js +608 -0
  23. package/src/product.js +27 -0
  24. package/src/project.js +156 -0
  25. package/src/sb3.js +340 -0
  26. package/src/sprite.js +81 -0
  27. package/vendor/pm-packager-dist/.fx-build.json +6 -0
  28. package/vendor/pm-packager-dist/LICENSE +202 -0
  29. package/vendor/pm-packager-dist/NOTICE +6 -0
  30. package/vendor/pm-packager-dist/dist/290e09e569a1cab8e61ba93b0d23863f.png +0 -0
  31. package/vendor/pm-packager-dist/dist/icns.js +1 -0
  32. package/vendor/pm-packager-dist/dist/packager.js +154 -0
  33. package/vendor/pm-packager-dist/dist/scaffolding/addons.js +2 -0
  34. package/vendor/pm-packager-dist/dist/scaffolding/extension worker.js +2 -0
  35. package/vendor/pm-packager-dist/dist/scaffolding/scaffolding-full.js +93 -0
  36. package/vendor/pm-packager-dist/dist/scaffolding/scaffolding-min.js +93 -0
  37. package/vendor/pm-packager-dist/dist/scaffolding/vendors~iframe-extension-worker.js +2 -0
  38. package/vendor/pm-packager-dist/dist/vendors~sha256.js +3 -0
  39. package/vendor/pm-packager-dist/package.json +3 -0
package/src/parser.js ADDED
@@ -0,0 +1,608 @@
1
+ /**
2
+ * Triflare Fx parser.
3
+ *
4
+ * Recursive-descent parser turning a token stream into a small, explicit AST.
5
+ * Triflare Fx is intentionally context-free: keywords are recognised by shape
6
+ * (`sprite "..."`, `when <event>`, `let ...`) rather than being reserved words.
7
+ */
8
+ import { tokenize } from "./lexer.js";
9
+ import { ParseError } from "./errors.js";
10
+
11
+ const ASSIGN_OPS = new Set(["=", "+=", "-=", "*=", "/="]);
12
+ const PROPERTY_KEYS = new Set(["layer", "costumes", "sounds"]);
13
+
14
+ class Parser {
15
+ /**
16
+ * @param {import('./lexer.js').Token[]} tokens
17
+ * @param {string} source
18
+ * @param {string} file
19
+ */
20
+ constructor(tokens, source, file) {
21
+ this.tokens = tokens;
22
+ this.source = source;
23
+ this.file = file;
24
+ this.pos = 0;
25
+ }
26
+
27
+ peek(offset = 0) {
28
+ return this.tokens[Math.min(this.pos + offset, this.tokens.length - 1)];
29
+ }
30
+
31
+ next() {
32
+ return this.tokens[this.pos++];
33
+ }
34
+
35
+ at(value, type) {
36
+ const tok = this.peek();
37
+ if (type && tok.type !== type) return false;
38
+ return tok.value === value;
39
+ }
40
+
41
+ atIdent(value) {
42
+ const tok = this.peek();
43
+ return tok.type === "ident" && tok.value === value;
44
+ }
45
+
46
+ atPunct(value) {
47
+ const tok = this.peek();
48
+ return tok.type === "punct" && tok.value === value;
49
+ }
50
+
51
+ eat(value, type) {
52
+ if (this.at(value, type)) {
53
+ return this.next();
54
+ }
55
+ return null;
56
+ }
57
+
58
+ expect(value, type) {
59
+ if (this.at(value, type)) return this.next();
60
+ this.fail(
61
+ `Expected ${JSON.stringify(value)} but found ${this.describe(this.peek())}`,
62
+ );
63
+ }
64
+
65
+ expectIdent() {
66
+ const tok = this.peek();
67
+ if (tok.type !== "ident")
68
+ this.fail(`Expected an identifier but found ${this.describe(tok)}`);
69
+ return this.next().value;
70
+ }
71
+
72
+ expectType(type, what = type) {
73
+ const tok = this.peek();
74
+ if (tok.type !== type)
75
+ this.fail(`Expected ${what} but found ${this.describe(tok)}`);
76
+ return this.next();
77
+ }
78
+
79
+ describe(tok) {
80
+ if (tok.type === "eof") return "end of file";
81
+ if (tok.type === "string") return `string ${JSON.stringify(tok.value)}`;
82
+ return JSON.stringify(tok.value);
83
+ }
84
+
85
+ fail(message, tok = this.peek()) {
86
+ throw new ParseError(message, {
87
+ file: this.file,
88
+ line: tok.line,
89
+ column: tok.column,
90
+ source: this.source,
91
+ });
92
+ }
93
+
94
+ // ------------------------------------------------------------------ core
95
+
96
+ parse() {
97
+ const sprites = [];
98
+ let stage = null;
99
+ const topItems = [];
100
+
101
+ while (this.peek().type !== "eof") {
102
+ if (
103
+ this.atIdent("partial") &&
104
+ this.peek(1).type === "ident" &&
105
+ this.peek(1).value === "sprite" &&
106
+ this.peek(2).type === "string"
107
+ ) {
108
+ const tok = this.next();
109
+ sprites.push(this.parseSprite(true, tok));
110
+ } else if (this.atIdent("sprite") && this.peek(1).type === "string") {
111
+ sprites.push(this.parseSprite());
112
+ } else if (
113
+ this.atIdent("stage") &&
114
+ this.peek(1).type === "punct" &&
115
+ this.peek(1).value === "{"
116
+ ) {
117
+ if (stage) this.fail("A project may only declare one stage");
118
+ stage = this.parseStage();
119
+ } else {
120
+ topItems.push(this.parseBodyItem());
121
+ }
122
+ }
123
+
124
+ return { type: "Program", stage, sprites, topItems };
125
+ }
126
+
127
+ parseSprite(partial = false, startTok = null) {
128
+ const tok = this.expect("sprite");
129
+ const name = this.expectType("string", "a sprite name string").value;
130
+ const body = this.parseBlockBody();
131
+ return { type: "SpriteDecl", name, body, loc: startTok ?? tok, partial };
132
+ }
133
+
134
+ parseStage() {
135
+ const tok = this.expect("stage");
136
+ const body = this.parseBlockBody();
137
+ return { type: "StageDecl", name: "Stage", body, loc: tok };
138
+ }
139
+
140
+ parseBlockBody() {
141
+ this.expect("{");
142
+ const items = [];
143
+ while (!this.atPunct("}")) {
144
+ if (this.peek().type === "eof")
145
+ this.fail('Unterminated block; expected "}"');
146
+ items.push(this.parseBodyItem());
147
+ }
148
+ this.expect("}");
149
+ return items;
150
+ }
151
+
152
+ parseBodyItem() {
153
+ const tok = this.peek();
154
+
155
+ if (tok.type === "ident") {
156
+ switch (tok.value) {
157
+ case "let":
158
+ return this.parseVarDecl(false);
159
+ case "cloud":
160
+ if (this.peek(1).type === "ident" && this.peek(1).value === "let") {
161
+ return this.parseVarDecl(true);
162
+ }
163
+ break;
164
+ case "list":
165
+ if (this.peek(1).type === "ident") {
166
+ return this.parseListDecl();
167
+ }
168
+ break;
169
+ case "function":
170
+ return this.parseFunctionDecl(false, false);
171
+ case "returning":
172
+ case "warp": {
173
+ const otherModifier =
174
+ tok.value === "returning" ? "warp" : "returning";
175
+ const combined =
176
+ this.peek(1).type === "ident" &&
177
+ this.peek(1).value === otherModifier;
178
+ if (
179
+ this.peek(combined ? 2 : 1).type === "ident" &&
180
+ this.peek(combined ? 2 : 1).value === "function"
181
+ ) {
182
+ return this.parseFunctionDecl(
183
+ tok.value === "returning" || combined,
184
+ tok.value === "warp" || combined,
185
+ );
186
+ }
187
+ break;
188
+ }
189
+ case "when":
190
+ return this.parseHat();
191
+ case "if":
192
+ return this.parseIf();
193
+ case "repeat":
194
+ return this.parseRepeat();
195
+ case "while":
196
+ return this.parseWhile();
197
+ case "forever":
198
+ return this.parseForever();
199
+ case "return":
200
+ return this.parseReturn();
201
+ default:
202
+ break;
203
+ }
204
+
205
+ if (
206
+ PROPERTY_KEYS.has(tok.value) &&
207
+ this.peek(1).type === "punct" &&
208
+ this.peek(1).value === ":"
209
+ ) {
210
+ return this.parseProperty();
211
+ }
212
+ }
213
+
214
+ return this.parseSimpleStatement();
215
+ }
216
+
217
+ parseVarDecl(cloud) {
218
+ const tok = cloud ? this.expect("cloud") : this.expect("let");
219
+ if (cloud) this.expect("let");
220
+ const name = this.expectIdent();
221
+ let init = null;
222
+ if (this.eat("=")) init = this.parseExpression();
223
+ this.expect(";");
224
+ return { type: "VarDecl", name, init, cloud, loc: tok };
225
+ }
226
+
227
+ parseListDecl() {
228
+ const tok = this.expect("list");
229
+ const name = this.expectIdent();
230
+ let items = [];
231
+ if (this.eat("=")) {
232
+ this.expect("[");
233
+ items = this.parseExpressionList("]");
234
+ this.expect("]");
235
+ }
236
+ this.expect(";");
237
+ return { type: "ListDecl", name, items, loc: tok };
238
+ }
239
+
240
+ parseFunctionDecl(returns, warp) {
241
+ const tok = this.peek();
242
+ while (this.atIdent("returning") || this.atIdent("warp")) this.next();
243
+ this.expect("function");
244
+ const name = this.expectIdent();
245
+ this.expect("(");
246
+ const params = [];
247
+ while (!this.atPunct(")")) {
248
+ const ptok = this.peek();
249
+ const paramName = this.expectIdent();
250
+ let boolean = false;
251
+ // Optional type annotation: `flag: boolean`
252
+ if (this.eat(":")) {
253
+ const typeName = this.expectIdent();
254
+ if (typeName === "boolean") boolean = true;
255
+ else if (typeName !== "number" && typeName !== "string") {
256
+ this.fail(`Unknown parameter type ${JSON.stringify(typeName)}`, ptok);
257
+ }
258
+ }
259
+ params.push({ name: paramName, boolean });
260
+ if (!this.eat(",")) break;
261
+ }
262
+ this.expect(")");
263
+ const body = this.parseBlockBody();
264
+ return {
265
+ type: "FunctionDecl",
266
+ name,
267
+ params,
268
+ returns,
269
+ warp,
270
+ body,
271
+ loc: tok,
272
+ };
273
+ }
274
+
275
+ parseHat() {
276
+ const tok = this.expect("when");
277
+ const event = this.parseCallOrIdentifier();
278
+ const body = this.parseBlockBody();
279
+ return { type: "HatDecl", event, body, loc: tok };
280
+ }
281
+
282
+ parseProperty() {
283
+ const tok = this.peek();
284
+ const key = this.expectIdent();
285
+ this.expect(":");
286
+ let value;
287
+ if (this.atPunct("{")) {
288
+ value = this.parseStringMap();
289
+ } else {
290
+ value = this.parseExpression();
291
+ }
292
+ this.expect(";");
293
+ return { type: "PropertyDecl", key, value, loc: tok };
294
+ }
295
+
296
+ parseStringMap() {
297
+ this.expect("{");
298
+ const map = [];
299
+ while (!this.atPunct("}")) {
300
+ const keyTok = this.peek();
301
+ let key;
302
+ if (keyTok.type === "ident") key = this.next().value;
303
+ else if (keyTok.type === "string") key = this.next().value;
304
+ else
305
+ this.fail(
306
+ `Expected a costume/sound name but found ${this.describe(keyTok)}`,
307
+ );
308
+ this.expect(":");
309
+ const valTok = this.expectType("string", "an asset path string");
310
+ map.push({ key, value: valTok.value, loc: keyTok });
311
+ if (!this.eat(",")) break;
312
+ }
313
+ this.expect("}");
314
+ return map;
315
+ }
316
+
317
+ // ------------------------------------------------------------ statements
318
+
319
+ parseIf() {
320
+ const tok = this.expect("if");
321
+ let cond;
322
+ if (this.eat("(")) {
323
+ cond = this.parseExpression();
324
+ this.expect(")");
325
+ } else {
326
+ cond = this.parseExpression();
327
+ }
328
+ const then = this.parseBlockBody();
329
+ let elseBody = null;
330
+ if (this.atIdent("else")) {
331
+ this.next();
332
+ if (this.atIdent("if")) {
333
+ elseBody = [this.parseIf()];
334
+ } else {
335
+ elseBody = this.parseBlockBody();
336
+ }
337
+ }
338
+ return { type: "If", cond, then, else: elseBody, loc: tok };
339
+ }
340
+
341
+ parseRepeat() {
342
+ const tok = this.expect("repeat");
343
+ let count;
344
+ if (this.eat("(")) {
345
+ count = this.parseExpression();
346
+ this.expect(")");
347
+ } else {
348
+ count = this.parseExpression();
349
+ }
350
+ const body = this.parseBlockBody();
351
+ return { type: "Repeat", count, body, loc: tok };
352
+ }
353
+
354
+ parseWhile() {
355
+ const tok = this.expect("while");
356
+ let cond;
357
+ if (this.eat("(")) {
358
+ cond = this.parseExpression();
359
+ this.expect(")");
360
+ } else {
361
+ cond = this.parseExpression();
362
+ }
363
+ const body = this.parseBlockBody();
364
+ return { type: "While", cond, body, loc: tok };
365
+ }
366
+
367
+ parseForever() {
368
+ const tok = this.expect("forever");
369
+ const body = this.parseBlockBody();
370
+ return { type: "Forever", body, loc: tok };
371
+ }
372
+
373
+ parseReturn() {
374
+ const tok = this.expect("return");
375
+ let value = null;
376
+ if (!this.atPunct(";")) value = this.parseExpression();
377
+ this.expect(";");
378
+ return { type: "Return", value, loc: tok };
379
+ }
380
+
381
+ parseSimpleStatement() {
382
+ const tok = this.peek();
383
+ const name = this.parseQualifiedName();
384
+
385
+ // Assignment to a (possibly scoped) variable.
386
+ if (
387
+ name.segments.length === 1 &&
388
+ this.peek().type === "punct" &&
389
+ ASSIGN_OPS.has(this.peek().value)
390
+ ) {
391
+ const op = this.next().value;
392
+ const value = this.parseExpression();
393
+ this.expect(";");
394
+ return { type: "Assign", name: name.segments[0], op, value, loc: tok };
395
+ }
396
+
397
+ let args = [];
398
+ if (this.eat("(")) {
399
+ args = this.parseExpressionList(")");
400
+ this.expect(")");
401
+ }
402
+
403
+ let body = null;
404
+ let elseBody = null;
405
+ if (this.atPunct("{")) {
406
+ body = this.parseBlockBody();
407
+ if (this.atIdent("else")) {
408
+ this.next();
409
+ if (this.atIdent("if")) elseBody = [this.parseIf()];
410
+ else elseBody = this.parseBlockBody();
411
+ }
412
+ this.eat(";");
413
+ } else {
414
+ this.expect(";");
415
+ }
416
+ return {
417
+ type: "BlockCall",
418
+ name: name.name,
419
+ segments: name.segments,
420
+ args,
421
+ body,
422
+ else: elseBody,
423
+ loc: tok,
424
+ };
425
+ }
426
+
427
+ parseCallOrIdentifier() {
428
+ const name = this.parseQualifiedName();
429
+ if (this.eat("(")) {
430
+ const args = this.parseExpressionList(")");
431
+ this.expect(")");
432
+ return { name: name.name, segments: name.segments, args };
433
+ }
434
+ return { name: name.name, segments: name.segments, args: [] };
435
+ }
436
+
437
+ parseQualifiedName() {
438
+ const tok = this.peek();
439
+ const segments = [this.expectIdent()];
440
+ while (this.atPunct(".") && this.peek(1).type === "ident") {
441
+ this.next();
442
+ segments.push(this.next().value);
443
+ }
444
+ return { name: segments.join("."), segments, loc: tok };
445
+ }
446
+
447
+ parseExpressionList(closing) {
448
+ const list = [];
449
+ if (this.atPunct(closing)) return list;
450
+ list.push(this.parseExpression());
451
+ while (this.eat(",")) {
452
+ if (this.atPunct(closing)) break; // tolerate trailing comma
453
+ list.push(this.parseExpression());
454
+ }
455
+ return list;
456
+ }
457
+
458
+ // ----------------------------------------------------------- expressions
459
+
460
+ parseExpression() {
461
+ return this.parseOr();
462
+ }
463
+
464
+ parseOr() {
465
+ let left = this.parseAnd();
466
+ while (this.atIdent("or") || this.atPunct("||")) {
467
+ this.next();
468
+ const right = this.parseAnd();
469
+ left = { type: "Binary", op: "or", left, right, loc: left.loc };
470
+ }
471
+ return left;
472
+ }
473
+
474
+ parseAnd() {
475
+ let left = this.parseNot();
476
+ while (this.atIdent("and") || this.atPunct("&&")) {
477
+ this.next();
478
+ const right = this.parseNot();
479
+ left = { type: "Binary", op: "and", left, right, loc: left.loc };
480
+ }
481
+ return left;
482
+ }
483
+
484
+ parseNot() {
485
+ if (this.atIdent("not") || this.atPunct("!")) {
486
+ const tok = this.next();
487
+ const operand = this.parseNot();
488
+ return { type: "Unary", op: "not", operand, loc: tok };
489
+ }
490
+ return this.parseComparison();
491
+ }
492
+
493
+ parseComparison() {
494
+ let left = this.parseAdditive();
495
+ while (
496
+ this.peek().type === "punct" &&
497
+ ["==", "!=", "<", "<=", ">", ">="].includes(this.peek().value)
498
+ ) {
499
+ const op = this.next().value;
500
+ const right = this.parseAdditive();
501
+ left = { type: "Binary", op, left, right, loc: left.loc };
502
+ }
503
+ return left;
504
+ }
505
+
506
+ parseAdditive() {
507
+ let left = this.parseMultiplicative();
508
+ while (
509
+ this.peek().type === "punct" &&
510
+ (this.peek().value === "+" || this.peek().value === "-")
511
+ ) {
512
+ const op = this.next().value;
513
+ const right = this.parseMultiplicative();
514
+ left = { type: "Binary", op, left, right, loc: left.loc };
515
+ }
516
+ return left;
517
+ }
518
+
519
+ parseMultiplicative() {
520
+ let left = this.parseUnary();
521
+ while (
522
+ this.peek().type === "punct" &&
523
+ ["*", "/", "%"].includes(this.peek().value)
524
+ ) {
525
+ const op = this.next().value;
526
+ const right = this.parseUnary();
527
+ left = { type: "Binary", op, left, right, loc: left.loc };
528
+ }
529
+ return left;
530
+ }
531
+
532
+ parseUnary() {
533
+ if (this.atPunct("-")) {
534
+ const tok = this.next();
535
+ const operand = this.parseUnary();
536
+ if (operand.type === "Number") {
537
+ return { type: "Number", value: -operand.value, loc: tok };
538
+ }
539
+ return { type: "Unary", op: "-", operand, loc: tok };
540
+ }
541
+ if (this.atPunct("+")) {
542
+ this.next();
543
+ return this.parseUnary();
544
+ }
545
+ return this.parsePrimary();
546
+ }
547
+
548
+ parsePrimary() {
549
+ const tok = this.peek();
550
+
551
+ if (tok.type === "number") {
552
+ this.next();
553
+ return { type: "Number", value: Number(tok.value), loc: tok };
554
+ }
555
+ if (tok.type === "string") {
556
+ this.next();
557
+ return { type: "String", value: tok.value, loc: tok };
558
+ }
559
+ if (tok.type === "punct" && tok.value === "(") {
560
+ this.next();
561
+ const expr = this.parseExpression();
562
+ this.expect(")");
563
+ return expr;
564
+ }
565
+ if (tok.type === "punct" && tok.value === "[") {
566
+ this.next();
567
+ const items = this.parseExpressionList("]");
568
+ this.expect("]");
569
+ return { type: "ArrayLiteral", items, loc: tok };
570
+ }
571
+ if (tok.type === "ident") {
572
+ if (tok.value === "true" || tok.value === "false") {
573
+ this.next();
574
+ return { type: "Boolean", value: tok.value === "true", loc: tok };
575
+ }
576
+ const name = this.parseQualifiedName();
577
+ if (this.eat("(")) {
578
+ const args = this.parseExpressionList(")");
579
+ this.expect(")");
580
+ return {
581
+ type: "Call",
582
+ name: name.name,
583
+ segments: name.segments,
584
+ args,
585
+ loc: tok,
586
+ };
587
+ }
588
+ return {
589
+ type: "Identifier",
590
+ name: name.name,
591
+ segments: name.segments,
592
+ loc: tok,
593
+ };
594
+ }
595
+
596
+ this.fail(`Unexpected ${this.describe(tok)} in expression`);
597
+ }
598
+ }
599
+
600
+ /**
601
+ * Parse a Triflare Fx source string into an AST.
602
+ * @param {string} source
603
+ * @param {string} [file]
604
+ */
605
+ export function parse(source, file = "<input>") {
606
+ const tokens = tokenize(source, file);
607
+ return new Parser(tokens, source, file).parse();
608
+ }
package/src/product.js ADDED
@@ -0,0 +1,27 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ import * as yaml from "js-yaml";
4
+
5
+ export function loadProduct() {
6
+ const productPath = fileURLToPath(new URL("../product.yml", import.meta.url));
7
+ return yaml.load(readFileSync(productPath, "utf8")) ?? {};
8
+ }
9
+
10
+ export function getProduct() {
11
+ return loadProduct();
12
+ }
13
+
14
+ export function productConfig() {
15
+ const product = loadProduct();
16
+ return {
17
+ name: product.name,
18
+ command: product.command,
19
+ version: product.version,
20
+ tagline: product.tagline,
21
+ author: product.author,
22
+ homepage: product.homepage,
23
+ branding: product.branding ?? {},
24
+ symbols: product.symbols ?? {},
25
+ defaults: product.defaults ?? {},
26
+ };
27
+ }