desmost 0.3.2 → 0.4.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 (61) hide show
  1. package/dist/compiler/compile.d.ts +2 -2
  2. package/dist/compiler/compile.d.ts.map +1 -1
  3. package/dist/compiler/compile.js +9 -4
  4. package/dist/compiler/compile.js.map +1 -1
  5. package/dist/compiler/evaluate.d.ts.map +1 -1
  6. package/dist/compiler/evaluate.js +1 -5
  7. package/dist/compiler/evaluate.js.map +1 -1
  8. package/dist/compiler/format.d.ts +4 -0
  9. package/dist/compiler/format.d.ts.map +1 -0
  10. package/dist/compiler/format.js +7 -0
  11. package/dist/compiler/format.js.map +1 -0
  12. package/dist/compiler/index.d.ts.map +1 -1
  13. package/dist/compiler/index.js.map +1 -1
  14. package/dist/compiler/options.d.ts +2 -2
  15. package/dist/compiler/options.d.ts.map +1 -1
  16. package/dist/compiler/options.js +11 -11
  17. package/dist/compiler/options.js.map +1 -1
  18. package/dist/index.internal.d.ts +1 -0
  19. package/dist/index.internal.d.ts.map +1 -1
  20. package/dist/index.internal.js.map +1 -1
  21. package/dist/magic/incantation.d.ts.map +1 -1
  22. package/dist/magic/incantation.js +2 -1
  23. package/dist/magic/incantation.js.map +1 -1
  24. package/dist/magic/index.d.ts.map +1 -1
  25. package/dist/magic/index.js.map +1 -1
  26. package/dist/magic/local/anim.d.ts +1 -1
  27. package/dist/magic/local/anim.d.ts.map +1 -1
  28. package/dist/magic/local/anim.js +1 -1
  29. package/dist/magic/local/anim.js.map +1 -1
  30. package/dist/magic/local/colour.js +1 -1
  31. package/dist/magic/local/colour.js.map +1 -1
  32. package/dist/magic/local/label.d.ts +1 -1
  33. package/dist/magic/local/label.d.ts.map +1 -1
  34. package/dist/magic/local/label.js +5 -10
  35. package/dist/magic/local/label.js.map +1 -1
  36. package/dist/parser/desmost-parser.d.ts +1 -1
  37. package/dist/parser/desmost-parser.d.ts.map +1 -1
  38. package/dist/parser/desmost-parser.js +110 -50
  39. package/dist/parser/desmost-parser.js.map +1 -1
  40. package/dist/parser/index.d.ts.map +1 -1
  41. package/dist/parser/index.js.map +1 -1
  42. package/dist/utils.d.ts +11 -0
  43. package/dist/utils.d.ts.map +1 -0
  44. package/dist/utils.js +41 -0
  45. package/dist/utils.js.map +1 -0
  46. package/package.json +5 -4
  47. package/src/compiler/compile.ts +12 -5
  48. package/src/compiler/evaluate.ts +1 -13
  49. package/src/compiler/format.ts +16 -0
  50. package/src/compiler/index.ts +7 -0
  51. package/src/compiler/options.ts +32 -20
  52. package/src/index.internal.ts +1 -0
  53. package/src/magic/incantation.ts +2 -1
  54. package/src/magic/index.ts +8 -0
  55. package/src/magic/local/anim.ts +1 -1
  56. package/src/magic/local/colour.ts +1 -1
  57. package/src/magic/local/label.ts +12 -7
  58. package/src/parser/desmost-parser.ts +173 -62
  59. package/src/parser/generic-parser.ts +3 -3
  60. package/src/parser/index.ts +0 -2
  61. package/src/utils.ts +54 -0
@@ -1,8 +1,10 @@
1
1
  import { GenericParser } from "./generic-parser";
2
2
  import { Ast } from "./ast";
3
3
 
4
+ import type { DesmostOptions } from "../compiler";
4
5
  import { FAIL, UnrecoverableError } from "../errors";
5
6
  import type { RecoverableFail, Unrecoverable, MaybeRecoverable } from "../errors";
7
+ import * as utils from "../utils";
6
8
 
7
9
  import {
8
10
  Incantation, ArgIncantation,
@@ -10,8 +12,6 @@ import {
10
12
  } from "../magic";
11
13
  import type { GLOBAL, LOCAL, EXPR } from "../magic";
12
14
 
13
- import type { DesmostOptions } from "../compiler";
14
-
15
15
 
16
16
  /**
17
17
  * A stateful lazy parser for Desmost.
@@ -318,24 +318,31 @@ export class DesmostParser extends GenericParser
318
318
  * ^^^^^^^
319
319
  * ```
320
320
  *
321
+ * Returns the raw text without `{}`.
322
+ *
323
+ * ```ts
324
+ * /incantation{ arg }
325
+ * ^^^
326
+ * ```
327
+ *
321
328
  * ## Notes
322
329
  *
323
330
  * We don't actually care for what type the argument is since we won't be evaluating it, we just want to find the closing `}`.
324
331
  *
325
- * However, the argument itself might be *meant* to contain a `}`. So we need a way to accurately identify the actual closing brace:
332
+ * However, the argument itself might contain one or many `}`! So we need a way to accurately identify the actual closing brace:
326
333
  *
327
334
  * ```ts
328
335
  * /incantation{ field: { value: 1 } }
329
336
  * ^ ^
330
337
  * ```
331
338
  *
332
- * A well-formed argument always has matching pairs of `{}`, so we'll keep a stack. It starts with the opening `{`. When the stack is depleted, we must've reached the end of the argument.
339
+ * In our context, we have the guarantee that any well-formed argument always has matching pairs of `{}`. So we'll keep track of a context stack that starts with the opening `{`, and when the stack is depleted, we must've reached the end of the argument.
333
340
  *
334
341
  * However, there's still more edge cases:
335
342
  *
336
343
  * ```ts
337
344
  * /label{ text: "This }s weird" }
338
- * ^
345
+ * ^
339
346
  * ```
340
347
  *
341
348
  * `{` and `}` can appear in strings, and they won't necessarily be matched, so we'll ignore them by also tracking string contexts.
@@ -345,9 +352,18 @@ export class DesmostParser extends GenericParser
345
352
  * ^
346
353
  * ```
347
354
  *
348
- * `{` and `}` in LaTeX can be escaped with `\{` or `\}`, and these won't necessarily be matched, so we'll ignore them by also tracking backslash escapes.
355
+ * `{` and `}` in LaTeX can be escaped with `\{` or `\}`, and these won't necessarily be matched, so we'll ignore them by also tracking escapes.
349
356
  *
350
357
  * If the user truly mismatches `{}`, then, well ...parsing will fail catastrophically!
358
+ *
359
+ * This method also handles parsing raw enum literals by converting them to strings. So this:
360
+ *
361
+ * ```ts
362
+ * /line{ style: DOTTED }
363
+ * ^^^^^^
364
+ * ```
365
+ *
366
+ * Produces `style: "DOTTED"` (instead of `style: DOTTED`, which would fail to later parse as JSON).
351
367
  */
352
368
  parse_incantation_arg(
353
369
  /**
@@ -367,68 +383,62 @@ export class DesmostParser extends GenericParser
367
383
  `Expected \`{\` to start incantation argument, but found: \`${this.preview()}\``
368
384
  );
369
385
 
370
- enum Ctx {
371
- BLOCK = Char.L_BRACE,
372
- STR_1 = Char.QUOTE_SINGLE,
373
- STR_2 = Char.QUOTE_DOUBLE,
374
- STR_F = Char.BACKTICK,
375
- ESCAPE = Char.BACKSLASH,
376
- }
386
+ /** The context stack to keep track of when the closing `}` is encountered. */
387
+ let stack = new ContextStack();
377
388
 
378
- let stack: Ctx[] = [Ctx.BLOCK];
379
-
380
- /**
381
- * Pop `ctx` if it is the currently active context, returning `true` if successful.
382
- */
383
- function try_pop(ctx: Ctx): boolean
384
- {
385
- if (stack.at(-1) === ctx) {
386
- stack.pop();
387
- return true;
388
- }
389
-
390
- return false;
391
- }
389
+ /** Indices at which to later insert `"` quotes. */
390
+ let indices_to_insert_quotes: number[] = [];
392
391
 
393
392
  while (stack.length > 0) {
394
- if (try_pop(Ctx.ESCAPE)) {
393
+ // If the last character was an escape, we don't care at all what the next character is!
394
+ // This handles \\ double escape perfectly fine, since the first negates the second
395
+ if (stack.try_pop(Ctx.ESCAPE)) {
395
396
  this.advance(
396
- `Unexpected end of input while parsing incantation argument, stack: ${JSON.stringify(stack)}`
397
+ `Unexpected end of input while parsing incantation argument, stack: ${stack.debug()}`
397
398
  );
398
399
  }
399
400
 
400
- let top = stack.at(-1)!;
401
-
402
- switch (this.current) {
403
- case Ctx.ESCAPE: stack.push(Ctx.ESCAPE); break;
401
+ let top = stack.top!;
404
402
 
405
- case Char.L_BRACE:
406
- /* NOTE: *Currently* `{}` should be ignored in all contexts except `Ctx.BLOCK`. This might change if more contexts are added in future! */
407
- if (top !== Ctx.BLOCK) break;
408
- stack.push(Ctx.BLOCK); break;
403
+ switch (this.current)
404
+ {
405
+ case Char.BACKSLASH: stack.push(Ctx.ESCAPE); break;
409
406
 
410
- case Char.R_BRACE:
411
- try_pop(Ctx.BLOCK); break;
407
+ case "{": stack.push( Ctx.BLOCK, { unless: [Ctx.STR_1, Ctx.STR_2, Ctx.STR_F] }); break;
408
+ case "}": stack.force_pop(Ctx.BLOCK, { unless: [Ctx.STR_1, Ctx.STR_2, Ctx.STR_F] }); break;
412
409
  }
413
410
 
414
- if (arg_type === Incantation.ArgType.OBJECT) {
415
- switch (this.current) {
416
- case Ctx.STR_1:
417
- if (top === Ctx.STR_2) break;
418
- if (top === Ctx.STR_F) break;
419
- try_pop(Ctx.STR_1) || stack.push(Ctx.STR_1);
411
+ if (arg_type === Incantation.ArgType.OBJECT)
412
+ {
413
+ if (top === Ctx.VALUE && this.current?.match(/[a-zA-Z]/)) {
414
+ stack.push(Ctx.ENUM);
415
+ indices_to_insert_quotes.push(this.i);
416
+ }
417
+ else if (top === Ctx.ENUM && this.current?.match(/[^a-zA-Z]/)) {
418
+ stack.pop();
419
+ indices_to_insert_quotes.push(this.i);
420
+
421
+ if (indices_to_insert_quotes.length % 2 != 0) {
422
+ console.error(`Desmost [INTERNAL]: Logic error in \`parse_incantation_arg()\`: Mismatched inserted quotes`);
423
+ }
424
+ }
425
+
426
+ switch (this.current)
427
+ {
428
+ case ":": stack.push(Ctx.VALUE, { when: [Ctx.BLOCK] }); break;
429
+ case ",": stack.try_pop(Ctx.VALUE); break;
430
+
431
+ case Char.QUOTE_1:
432
+ stack.pop_or_push(Ctx.STR_1, { unless: [Ctx.STR_2, Ctx.STR_F] });
433
+ stack.try_pop(Ctx.VALUE);
420
434
  break;
421
-
422
- case Ctx.STR_2:
423
- if (top === Ctx.STR_1) break;
424
- if (top === Ctx.STR_F) break;
425
- try_pop(Ctx.STR_2) || stack.push(Ctx.STR_2);
435
+ case Char.QUOTE_2:
436
+ stack.pop_or_push(Ctx.STR_2, { unless: [Ctx.STR_1, Ctx.STR_F] });
437
+ stack.try_pop(Ctx.VALUE);
426
438
  break;
427
-
428
- case Ctx.STR_F:
429
- if (top === Ctx.STR_1) break;
430
- if (top === Ctx.STR_2) break;
431
- try_pop(Ctx.STR_F) || stack.push(Ctx.STR_F);
439
+ case Char.BACKTICK:
440
+ stack.pop_or_push(Ctx.STR_F, { unless: [Ctx.STR_1, Ctx.STR_2] });
441
+ stack.try_pop(Ctx.VALUE);
432
442
  break;
433
443
  }
434
444
  }
@@ -439,18 +449,119 @@ export class DesmostParser extends GenericParser
439
449
  }
440
450
 
441
451
  /* NOTE: Cut in by 1 on both sides to exclude {} braces */
442
- return this.source.slice(init + 1, this.i - 1).trim();
452
+ let out = utils.chars(this.source, init + 1, this.i - 1);
453
+
454
+ for (let [i, j] of utils.reversing(utils.paired(indices_to_insert_quotes))) {
455
+ i -= init + 1;
456
+ j -= init + 1;
457
+
458
+ let value = out.slice(i, j).join("");
459
+ if (["true", "false", "null", "undefined"].includes(value)) continue;
460
+
461
+ out.splice(j, 0, `"`);
462
+ out.splice(i, 0, `"`);
463
+ }
464
+
465
+ return out.join("").trim();
443
466
  }
467
+ }
444
468
 
469
+
470
+ class ContextStack
471
+ {
472
+ #data: Ctx[] = [Ctx.BLOCK]
473
+
474
+ get length(): number {
475
+ return this.#data.length;
476
+ }
477
+
478
+ /** The currently active context. */
479
+ get top(): Ctx {
480
+ return this.#data.at(-1)!;
481
+ }
482
+
483
+ push(ctx: Ctx, options?: StackOperationOptions): boolean
484
+ {
485
+ if (this.#should_skip(options)) return false;
486
+
487
+ this.#data.push(ctx);
488
+ return true;
489
+ }
490
+
491
+ pop(): void
492
+ {
493
+ this.#data.pop();
494
+ }
495
+
496
+ /** Pop `ctx` if it is the currently active context, returning `true` if so. */
497
+ try_pop(ctx: Ctx): boolean
498
+ {
499
+ if (this.top === ctx) {
500
+ this.#data.pop();
501
+ return true;
502
+ } else {
503
+ return false;
504
+ }
505
+ }
506
+
507
+ /** Backtrack until `ctx` is popped. */
508
+ force_pop(ctx: Ctx, options?: StackOperationOptions): boolean
509
+ {
510
+ if (this.#should_skip(options)) return false;
511
+
512
+ let idx = this.#data.lastIndexOf(ctx);
513
+ if (idx === -1) return false;
514
+
515
+ /* NOTE: We could report errors for unterminated contexts, but we'll leave that for the actual evaluation - in case we get something wrong ;) */
516
+ this.#data.splice(idx);
517
+ return true;
518
+ }
519
+
520
+ /** Pop `ctx` if it is the current context, else push it onto the stack. */
521
+ pop_or_push(ctx: Ctx, options?: StackOperationOptions): boolean
522
+ {
523
+ if (this.#should_skip(options)) return false;
524
+
525
+ this.try_pop(ctx) || this.push(ctx);
526
+ return true;
527
+ }
528
+
529
+ debug(): string {
530
+ return JSON.stringify(this.#data);
531
+ }
532
+
533
+ #should_skip(options: StackOperationOptions | undefined): boolean
534
+ {
535
+ return Boolean(
536
+ options?.when != undefined && !options?.when?.includes(this.top)
537
+ || options?.unless?.includes(this.top)
538
+ );
539
+ }
445
540
  }
446
541
 
542
+ interface StackOperationOptions
543
+ {
544
+ /** Only perform this operation if the current context is any of these. */
545
+ when?: Ctx[];
546
+
547
+ /** Don't perform this operation if the current context is any of these. */
548
+ unless?: Ctx[];
549
+ }
447
550
 
448
551
  enum Char
449
552
  {
450
- L_BRACE = "{",
451
- R_BRACE = "}",
452
- QUOTE_SINGLE = `'`,
453
- QUOTE_DOUBLE = `"`,
454
- BACKTICK = "`",
455
- BACKSLASH = "\\",
553
+ BACKSLASH = "\\",
554
+ QUOTE_1 = `'`,
555
+ QUOTE_2 = `"`,
556
+ BACKTICK = "`",
557
+ }
558
+
559
+ enum Ctx {
560
+ BLOCK = "{",
561
+ VALUE = ":",
562
+ ENUM = "<enum literal>",
563
+ STR_1 = Char.QUOTE_1,
564
+ STR_2 = Char.QUOTE_2,
565
+ STR_F = Char.BACKTICK,
566
+ ESCAPE = Char.BACKSLASH,
456
567
  }
@@ -8,9 +8,9 @@ const IGNORED_CHARACTERS = new Set([
8
8
 
9
9
 
10
10
  /**
11
- * A stateful lazy parser, providing core methods for parsing.
11
+ * A stateful lazy parser, providing core (non-Desmost-specific) methods for parsing.
12
12
  *
13
- * Many methods come in `<verb>` and `try_<verb>` pairs. On parsing failure, the former throws an `UnrecoverableError`, but the latter instead backtracks and only throws a `RecoverableFail`.
13
+ * Many methods come in `<verb>` and `try_<verb>` pairs. On parsing failure, the former throws an `UnrecoverableError`, but the latter instead backtracks and only returns a `RecoverableFail`. The former is for expected parses, while the latter is for speculative parsing.
14
14
  */
15
15
  export class GenericParser
16
16
  {
@@ -57,7 +57,7 @@ export class GenericParser
57
57
  }
58
58
 
59
59
  /**
60
- * Peek a snippet of the upcoming source text (for error messages).
60
+ * Peek a snippet of the upcoming source text.
61
61
  *
62
62
  * Optionally start from the given `idx`.
63
63
  */
@@ -1,7 +1,5 @@
1
1
  /**
2
2
  * Implements `DesmostParser`, the Desmost parsing engine.
3
- *
4
- * The parsing engine makes heavy use of exceptions for backtracking, which is *very unperformant*, but makes for *much more readable code*. Desmost isn't expecting to parse huge documents of source code, so I decided this is an acceptable trade-off.
5
3
  */
6
4
 
7
5
  export { DesmostParser } from "./desmost-parser";
package/src/utils.ts ADDED
@@ -0,0 +1,54 @@
1
+ export function* reversing<T>(items: ArrayLike<T>): Generator<T>
2
+ {
3
+ for (let i = items.length - 1; i >= 0; i--) {
4
+ yield items[i];
5
+ }
6
+ }
7
+
8
+ export function paired<T>(items: Iterable<T>): Array<[left: T, right: T]>
9
+ {
10
+ return pairing(items).toArray();
11
+ }
12
+
13
+ export function* pairing<T>(items: Iterable<T>): Generator<[left: T, right: T]>
14
+ {
15
+ let previous = null;
16
+
17
+ for (let item of items) {
18
+ if (previous === null) {
19
+ previous = item;
20
+ continue;
21
+ }
22
+ else {
23
+ yield [previous, item];
24
+ previous = null;
25
+ }
26
+ }
27
+
28
+ if (previous !== null) {
29
+ return previous;
30
+ }
31
+ }
32
+
33
+
34
+ export function chars(s: string, start?: number, stop?: number): string[]
35
+ {
36
+ return new Range(start ?? 0, stop ?? s.length).map(i => s[i]).toArray();
37
+ }
38
+
39
+
40
+ export class Range
41
+ {
42
+ constructor(
43
+ protected start: number,
44
+ protected stop: number,
45
+ )
46
+ {}
47
+
48
+ *map<T>(callback: (n: number) => T): Generator<T>
49
+ {
50
+ for (let n = this.start; n < this.stop; n++) {
51
+ yield callback(n);
52
+ }
53
+ }
54
+ }