desmost 0.3.2 → 0.4.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.
Files changed (56) hide show
  1. package/dist/compiler/compile.d.ts.map +1 -1
  2. package/dist/compiler/compile.js +4 -2
  3. package/dist/compiler/compile.js.map +1 -1
  4. package/dist/compiler/evaluate.d.ts.map +1 -1
  5. package/dist/compiler/evaluate.js +1 -5
  6. package/dist/compiler/evaluate.js.map +1 -1
  7. package/dist/compiler/format.d.ts +4 -0
  8. package/dist/compiler/format.d.ts.map +1 -0
  9. package/dist/compiler/format.js +7 -0
  10. package/dist/compiler/format.js.map +1 -0
  11. package/dist/compiler/index.d.ts.map +1 -1
  12. package/dist/compiler/index.js.map +1 -1
  13. package/dist/compiler/options.d.ts +1 -1
  14. package/dist/compiler/options.d.ts.map +1 -1
  15. package/dist/compiler/options.js +1 -1
  16. package/dist/compiler/options.js.map +1 -1
  17. package/dist/magic/incantation.d.ts.map +1 -1
  18. package/dist/magic/incantation.js +2 -1
  19. package/dist/magic/incantation.js.map +1 -1
  20. package/dist/magic/index.d.ts.map +1 -1
  21. package/dist/magic/index.js.map +1 -1
  22. package/dist/magic/local/anim.d.ts +1 -1
  23. package/dist/magic/local/anim.d.ts.map +1 -1
  24. package/dist/magic/local/anim.js +1 -1
  25. package/dist/magic/local/anim.js.map +1 -1
  26. package/dist/magic/local/colour.js +1 -1
  27. package/dist/magic/local/colour.js.map +1 -1
  28. package/dist/magic/local/label.d.ts +1 -1
  29. package/dist/magic/local/label.d.ts.map +1 -1
  30. package/dist/magic/local/label.js +5 -10
  31. package/dist/magic/local/label.js.map +1 -1
  32. package/dist/parser/desmost-parser.d.ts +1 -1
  33. package/dist/parser/desmost-parser.d.ts.map +1 -1
  34. package/dist/parser/desmost-parser.js +110 -50
  35. package/dist/parser/desmost-parser.js.map +1 -1
  36. package/dist/parser/index.d.ts.map +1 -1
  37. package/dist/parser/index.js.map +1 -1
  38. package/dist/utils.d.ts +11 -0
  39. package/dist/utils.d.ts.map +1 -0
  40. package/dist/utils.js +41 -0
  41. package/dist/utils.js.map +1 -0
  42. package/package.json +1 -1
  43. package/src/compiler/compile.ts +4 -2
  44. package/src/compiler/evaluate.ts +1 -13
  45. package/src/compiler/format.ts +16 -0
  46. package/src/compiler/index.ts +7 -0
  47. package/src/compiler/options.ts +22 -10
  48. package/src/magic/incantation.ts +2 -1
  49. package/src/magic/index.ts +8 -0
  50. package/src/magic/local/anim.ts +1 -1
  51. package/src/magic/local/colour.ts +1 -1
  52. package/src/magic/local/label.ts +12 -7
  53. package/src/parser/desmost-parser.ts +173 -62
  54. package/src/parser/generic-parser.ts +3 -3
  55. package/src/parser/index.ts +0 -2
  56. package/src/utils.ts +54 -0
package/dist/utils.js ADDED
@@ -0,0 +1,41 @@
1
+ export function* reversing(items) {
2
+ for (let i = items.length - 1; i >= 0; i--) {
3
+ yield items[i];
4
+ }
5
+ }
6
+ export function paired(items) {
7
+ return pairing(items).toArray();
8
+ }
9
+ export function* pairing(items) {
10
+ let previous = null;
11
+ for (let item of items) {
12
+ if (previous === null) {
13
+ previous = item;
14
+ continue;
15
+ }
16
+ else {
17
+ yield [previous, item];
18
+ previous = null;
19
+ }
20
+ }
21
+ if (previous !== null) {
22
+ return previous;
23
+ }
24
+ }
25
+ export function chars(s, start, stop) {
26
+ return new Range(start ?? 0, stop ?? s.length).map(i => s[i]).toArray();
27
+ }
28
+ export class Range {
29
+ start;
30
+ stop;
31
+ constructor(start, stop) {
32
+ this.start = start;
33
+ this.stop = stop;
34
+ }
35
+ *map(callback) {
36
+ for (let n = this.start; n < this.stop; n++) {
37
+ yield callback(n);
38
+ }
39
+ }
40
+ }
41
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,MAAM,SAAS,CAAC,CAAC,SAAS,CAAI,KAAmB;IAE/C,KAAK,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3C,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,MAAM,CAAI,KAAkB;IAE1C,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;AAClC,CAAC;AAED,MAAM,SAAS,CAAC,CAAC,OAAO,CAAI,KAAkB;IAE5C,IAAI,QAAQ,GAAG,IAAI,CAAC;IAEpB,KAAK,IAAI,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YACtB,QAAQ,GAAG,IAAI,CAAC;YAChB,SAAS;QACX,CAAC;aACI,CAAC;YACJ,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YACvB,QAAQ,GAAG,IAAI,CAAC;QAClB,CAAC;IACH,CAAC;IAED,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,OAAO,QAAQ,CAAC;IAClB,CAAC;AACH,CAAC;AAGD,MAAM,UAAU,KAAK,CAAC,CAAS,EAAE,KAAc,EAAE,IAAa;IAE5D,OAAO,IAAI,KAAK,CAAC,KAAK,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;AAC1E,CAAC;AAGD,MAAM,OAAO,KAAK;IAGJ;IACA;IAFZ,YACY,KAAa,EACb,IAAY;QADZ,UAAK,GAAL,KAAK,CAAQ;QACb,SAAI,GAAJ,IAAI,CAAQ;IAEvB,CAAC;IAEF,CAAC,GAAG,CAAI,QAA0B;QAEhC,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5C,MAAM,QAAQ,CAAC,CAAC,CAAC,CAAC;QACpB,CAAC;IACH,CAAC;CACF"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "desmost",
3
3
  "author": "Sup#2.0",
4
- "version": "0.3.2",
4
+ "version": "0.4.0",
5
5
  "license": "MIT",
6
6
 
7
7
  "description": "A tiny DSL for compiling LaTeX to Desmos",
@@ -65,7 +65,7 @@ export function compile(
65
65
  ast.push(r);
66
66
  }
67
67
 
68
- let defer: string | void;
68
+ let defer: string | void = undefined;
69
69
 
70
70
  switch (r.kind) {
71
71
  case Ast.Kind.INCANTATION_INVOCATION:
@@ -73,6 +73,8 @@ export function compile(
73
73
  break;
74
74
 
75
75
  case Ast.Kind.EXPRESSION:
76
+ // @ts-expect-error: outdated types
77
+ if (opts.ignore_blank_lines && r.data.latex === " ") break;
76
78
  defer = evaluate_expr(r, desmos, opts);
77
79
  break;
78
80
 
@@ -88,7 +90,7 @@ export function compile(
88
90
  }
89
91
  catch (e) {
90
92
  if (e instanceof UnrecoverableError) {
91
- errors = [e.message];
93
+ errors.push(e);
92
94
  } else {
93
95
  throw e;
94
96
  }
@@ -1,3 +1,4 @@
1
+ import { format_error } from "./format";
1
2
  import type { DesmostOptions } from "./options";
2
3
 
3
4
  import { Ast } from "../parser";
@@ -153,16 +154,3 @@ function prettify_latex(latex: string): string
153
154
 
154
155
  return latex;
155
156
  }
156
-
157
-
158
- function format_error(
159
- e: UnrecoverableError,
160
- options: Required<DesmostOptions>,
161
- ): string
162
- {
163
- let prefix = options.error_prefix;
164
- let sep = (prefix === "" || prefix.endsWith("\n")) ? "" : " ";
165
-
166
- return `${prefix}${sep}${e.name}: ${e.message}`;
167
- // TODO maybe include stack? (configurable?)
168
- }
@@ -0,0 +1,16 @@
1
+ import type { DesmostOptions } from "./options";
2
+
3
+ import { UnrecoverableError } from "../errors";
4
+
5
+
6
+ export function format_error(
7
+ e: UnrecoverableError,
8
+ options: Required<DesmostOptions>,
9
+ ): string
10
+ {
11
+ let prefix = options.error_prefix;
12
+ let sep = (prefix === "" || prefix.endsWith("\n")) ? "" : " ";
13
+
14
+ return `${prefix}${sep}${e.name}: ${e.message}`;
15
+ // TODO maybe include stack? (configurable?)
16
+ }
@@ -1,2 +1,9 @@
1
+ /**
2
+ * Implements the top-level logic of the Desmost compiler.
3
+ */
4
+
5
+ // public entry point
1
6
  export { compile } from "./compile";
7
+
8
+ // compilation options
2
9
  export type { DesmostOptions } from "./options";
@@ -1,10 +1,16 @@
1
- /** Options to customise Desmost compilation. */
1
+ /**
2
+ * Options to customise Desmost compilation.
3
+ *
4
+ * To customise compilation, pass in a `DesmostOptions` object as the last argument to `compile()`:
5
+ *
6
+ * ```ts
7
+ * compile(calc, source, { errors: "crash" });
8
+ * ```
9
+ */
2
10
  export interface DesmostOptions
3
11
  {
4
12
  /**
5
- * How should errors be *surfaced*?
6
- *
7
- * Errors will always be logged to console for the developer; this setting affects how they visually reach the end user.
13
+ * How should errors be *surfaced* to the end user?
8
14
  *
9
15
  * - `surface` (default): Blocks that result in errors will become Desmos text expressions containing the error message, leaving other expressions unaffected.
10
16
  * - `crash`: The entire compilation to Desmos will terminate with a single error message. This means you don't get any output at all, but errors are also immediately obvious.
@@ -22,7 +28,11 @@ export interface DesmostOptions
22
28
  place_errors?: "inline" | "end" | "start"
23
29
 
24
30
  /**
25
- * The prefix to prepend to error blocks. Defaults to `[DESMOST ERROR]\n`. Provide a blank string if you wish for no prefix to be added.
31
+ * The prefix to prepend to error blocks.
32
+ *
33
+ * Defaults to `[DESMOST ERROR]\n`.
34
+ *
35
+ * Provide a `""` blank string if you wish for no prefix to be added.
26
36
  */
27
37
  error_prefix?: string
28
38
 
@@ -34,11 +44,11 @@ export interface DesmostOptions
34
44
  ignore_comments?: boolean
35
45
 
36
46
  /**
37
- * Should all line breaks be ignored, instead of kept as blank expressions.
47
+ * Should all line breaks be ignored, instead of kept as blank expressions?
38
48
  *
39
49
  * Defaults to `false`, meaning all blank lines are kept.
40
50
  */
41
- ignore_line_breaks?: boolean
51
+ ignore_blank_lines?: boolean
42
52
 
43
53
  /**
44
54
  * Should trailing blank lines at the end of the source be ignored, instead of kept as blank expressions?
@@ -55,9 +65,11 @@ export interface DesmostOptions
55
65
  prettify?: boolean
56
66
 
57
67
  /**
58
- * Return debug diagnostics? This includes the unevaluated AST, and performance diagnostics.
68
+ * Return debug diagnostics?
69
+ *
70
+ * This includes the unevaluated AST, and performance diagnostics.
59
71
  *
60
- * Defaults to `false`.
72
+ * Defaults to `false`, meaning `compile()` returns `void`.
61
73
  */
62
74
  debug?: boolean
63
75
  }
@@ -75,7 +87,7 @@ export function set_default_options(options: Partial<DesmostOptions> | undefined
75
87
  options.error_prefix ??= "[DESMOST ERROR]\n";
76
88
 
77
89
  options.ignore_comments ??= false;
78
- options.ignore_line_breaks ??= false;
90
+ options.ignore_blank_lines ??= false;
79
91
  options.ignore_trailing_blanks ??= false;
80
92
 
81
93
  options.prettify ??= true;
@@ -83,7 +83,7 @@ export abstract class ArgIncantation<
83
83
  case Incantation.ArgType.LATEX: return raw;
84
84
  case Incantation.ArgType.ENUM: return raw;
85
85
 
86
- case Incantation.ArgType.OBJECT:
86
+ case Incantation.ArgType.OBJECT: {
87
87
  try {
88
88
  return Json5.parse(`{${raw}}`);
89
89
  }
@@ -91,6 +91,7 @@ export abstract class ArgIncantation<
91
91
  // @ts-expect-error: fine
92
92
  throw new UnrecoverableError.InvalidArgument(e.message);
93
93
  }
94
+ }
94
95
  }
95
96
  }
96
97
  }
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Implements incantations logic.
3
+ *
4
+ * Incantations are represented with `Incantation` and `ArgIncantation` objects. Individual incantations derive from one of these classes, defining their own evaluation and application logic.
5
+ *
6
+ * Defined incantations are listed under `*_INCANTATIONS` lists.
7
+ */
8
+
1
9
  export { Incantation, ArgIncantation } from "./incantation";
2
10
  export type { GLOBAL, LOCAL, EXPR } from "./incantation";
3
11
 
@@ -4,7 +4,7 @@ import { Incantation, type LOCAL } from "../incantation";
4
4
  export class AnimIncantation extends Incantation<LOCAL>
5
5
  {
6
6
  override readonly description
7
- = "Animates the slider of a block."
7
+ = "Animate the slider of a block."
8
8
 
9
9
  override readonly identifier = "anim"
10
10
 
@@ -22,7 +22,7 @@ export class ColourIncantation extends ArgIncantation<LOCAL>
22
22
 
23
23
  override evaluate_arg(data: string): DesmosColour
24
24
  {
25
- switch (data.trim()) {
25
+ switch (data.trim().toUpperCase()) {
26
26
  case "RED": return DesmosColour.RED;
27
27
  case "BLUE": return DesmosColour.BLUE;
28
28
  case "GREEN": return DesmosColour.GREEN;
@@ -14,7 +14,7 @@ interface LabelOptions
14
14
  export class LabelIncantation extends ArgIncantation<LOCAL>
15
15
  {
16
16
  override readonly description
17
- = ""
17
+ = "Label a block that evaluates to a point(s)."
18
18
 
19
19
  override readonly identifier = "label"
20
20
  override readonly requires_arg = true
@@ -41,14 +41,19 @@ export class LabelIncantation extends ArgIncantation<LOCAL>
41
41
  }
42
42
 
43
43
  if (typeof out.pos != "undefined") {
44
- switch (out.pos.trim().toUpperCase()) {
45
- case "ABOVE": out.pos = Desmos.LabelOrientations.ABOVE; break;
46
- case "BELOW": out.pos = Desmos.LabelOrientations.BELOW; break;
47
- case "LEFT": out.pos = Desmos.LabelOrientations.LEFT; break;
48
- case "RIGHT": out.pos = Desmos.LabelOrientations.RIGHT; break;
44
+ let pos = out.pos.trim().toUpperCase();
45
+
46
+ switch (pos) {
47
+ case "ABOVE":
48
+ case "BELOW":
49
+ case "LEFT":
50
+ case "RIGHT":
51
+ out.pos = pos;
52
+ break;
53
+
49
54
  default:
50
55
  throw new UnrecoverableError.InvalidArgument(
51
- `/label: Invalid label position: ${out.pos}`
56
+ `/label: Invalid label position: ${pos}`
52
57
  );
53
58
  }
54
59
  }
@@ -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";