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.
- package/dist/compiler/compile.d.ts.map +1 -1
- package/dist/compiler/compile.js +4 -2
- 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 +1 -1
- package/dist/compiler/options.d.ts.map +1 -1
- package/dist/compiler/options.js +1 -1
- package/dist/compiler/options.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 +1 -1
- package/src/compiler/compile.ts +4 -2
- 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 +22 -10
- 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
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
package/src/compiler/compile.ts
CHANGED
|
@@ -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
|
|
93
|
+
errors.push(e);
|
|
92
94
|
} else {
|
|
93
95
|
throw e;
|
|
94
96
|
}
|
package/src/compiler/evaluate.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/compiler/index.ts
CHANGED
package/src/compiler/options.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
/**
|
|
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.
|
|
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
|
-
|
|
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?
|
|
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.
|
|
90
|
+
options.ignore_blank_lines ??= false;
|
|
79
91
|
options.ignore_trailing_blanks ??= false;
|
|
80
92
|
|
|
81
93
|
options.prettify ??= true;
|
package/src/magic/incantation.ts
CHANGED
|
@@ -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
|
}
|
package/src/magic/index.ts
CHANGED
|
@@ -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
|
|
package/src/magic/local/anim.ts
CHANGED
|
@@ -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
|
-
= "
|
|
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;
|
package/src/magic/local/label.ts
CHANGED
|
@@ -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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
case "
|
|
48
|
-
case "
|
|
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: ${
|
|
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
|
|
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";
|