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.
- package/dist/compiler/compile.d.ts +2 -2
- package/dist/compiler/compile.d.ts.map +1 -1
- package/dist/compiler/compile.js +9 -4
- package/dist/compiler/compile.js.map +1 -1
- package/dist/compiler/evaluate.d.ts.map +1 -1
- package/dist/compiler/evaluate.js +1 -5
- package/dist/compiler/evaluate.js.map +1 -1
- package/dist/compiler/format.d.ts +4 -0
- package/dist/compiler/format.d.ts.map +1 -0
- package/dist/compiler/format.js +7 -0
- package/dist/compiler/format.js.map +1 -0
- package/dist/compiler/index.d.ts.map +1 -1
- package/dist/compiler/index.js.map +1 -1
- package/dist/compiler/options.d.ts +2 -2
- package/dist/compiler/options.d.ts.map +1 -1
- package/dist/compiler/options.js +11 -11
- package/dist/compiler/options.js.map +1 -1
- package/dist/index.internal.d.ts +1 -0
- package/dist/index.internal.d.ts.map +1 -1
- package/dist/index.internal.js.map +1 -1
- package/dist/magic/incantation.d.ts.map +1 -1
- package/dist/magic/incantation.js +2 -1
- package/dist/magic/incantation.js.map +1 -1
- package/dist/magic/index.d.ts.map +1 -1
- package/dist/magic/index.js.map +1 -1
- package/dist/magic/local/anim.d.ts +1 -1
- package/dist/magic/local/anim.d.ts.map +1 -1
- package/dist/magic/local/anim.js +1 -1
- package/dist/magic/local/anim.js.map +1 -1
- package/dist/magic/local/colour.js +1 -1
- package/dist/magic/local/colour.js.map +1 -1
- package/dist/magic/local/label.d.ts +1 -1
- package/dist/magic/local/label.d.ts.map +1 -1
- package/dist/magic/local/label.js +5 -10
- package/dist/magic/local/label.js.map +1 -1
- package/dist/parser/desmost-parser.d.ts +1 -1
- package/dist/parser/desmost-parser.d.ts.map +1 -1
- package/dist/parser/desmost-parser.js +110 -50
- package/dist/parser/desmost-parser.js.map +1 -1
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js.map +1 -1
- package/dist/utils.d.ts +11 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +41 -0
- package/dist/utils.js.map +1 -0
- package/package.json +5 -4
- package/src/compiler/compile.ts +12 -5
- package/src/compiler/evaluate.ts +1 -13
- package/src/compiler/format.ts +16 -0
- package/src/compiler/index.ts +7 -0
- package/src/compiler/options.ts +32 -20
- package/src/index.internal.ts +1 -0
- package/src/magic/incantation.ts +2 -1
- package/src/magic/index.ts +8 -0
- package/src/magic/local/anim.ts +1 -1
- package/src/magic/local/colour.ts +1 -1
- package/src/magic/local/label.ts +12 -7
- package/src/parser/desmost-parser.ts +173 -62
- package/src/parser/generic-parser.ts +3 -3
- package/src/parser/index.ts +0 -2
- 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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
371
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: ${
|
|
397
|
+
`Unexpected end of input while parsing incantation argument, stack: ${stack.debug()}`
|
|
397
398
|
);
|
|
398
399
|
}
|
|
399
400
|
|
|
400
|
-
let top = stack.
|
|
401
|
-
|
|
402
|
-
switch (this.current) {
|
|
403
|
-
case Ctx.ESCAPE: stack.push(Ctx.ESCAPE); break;
|
|
401
|
+
let top = stack.top!;
|
|
404
402
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
stack.push(Ctx.BLOCK); break;
|
|
403
|
+
switch (this.current)
|
|
404
|
+
{
|
|
405
|
+
case Char.BACKSLASH: stack.push(Ctx.ESCAPE); break;
|
|
409
406
|
|
|
410
|
-
case
|
|
411
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
423
|
-
|
|
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
|
-
|
|
429
|
-
|
|
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
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
|
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
|
|
60
|
+
* Peek a snippet of the upcoming source text.
|
|
61
61
|
*
|
|
62
62
|
* Optionally start from the given `idx`.
|
|
63
63
|
*/
|
package/src/parser/index.ts
CHANGED
|
@@ -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
|
+
}
|