desmost 0.1.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 +32 -0
- package/dist/compiler/compile.d.ts.map +1 -0
- package/dist/compiler/compile.js +101 -0
- package/dist/compiler/compile.js.map +1 -0
- package/dist/compiler/evaluate.d.ts +18 -0
- package/dist/compiler/evaluate.d.ts.map +1 -0
- package/dist/compiler/evaluate.js +83 -0
- package/dist/compiler/evaluate.js.map +1 -0
- package/dist/compiler/index.d.ts +3 -0
- package/dist/compiler/index.d.ts.map +1 -0
- package/dist/compiler/index.js +2 -0
- package/dist/compiler/index.js.map +1 -0
- package/dist/compiler/options.d.ts +54 -0
- package/dist/compiler/options.d.ts.map +1 -0
- package/dist/compiler/options.js +15 -0
- package/dist/compiler/options.js.map +1 -0
- package/dist/desmos.d.ts +12 -0
- package/dist/desmos.d.ts.map +1 -0
- package/dist/desmos.js +13 -0
- package/dist/desmos.js.map +1 -0
- package/dist/errors.d.ts +45 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +38 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/magic/expr/index.d.ts +3 -0
- package/dist/magic/expr/index.d.ts.map +1 -0
- package/dist/magic/expr/index.js +8 -0
- package/dist/magic/expr/index.js.map +1 -0
- package/dist/magic/expr/latex.d.ts +10 -0
- package/dist/magic/expr/latex.d.ts.map +1 -0
- package/dist/magic/expr/latex.js +16 -0
- package/dist/magic/expr/latex.js.map +1 -0
- package/dist/magic/expr/text.d.ts +9 -0
- package/dist/magic/expr/text.d.ts.map +1 -0
- package/dist/magic/expr/text.js +13 -0
- package/dist/magic/expr/text.js.map +1 -0
- package/dist/magic/global/dark-mode.d.ts +7 -0
- package/dist/magic/global/dark-mode.d.ts.map +1 -0
- package/dist/magic/global/dark-mode.js +11 -0
- package/dist/magic/global/dark-mode.js.map +1 -0
- package/dist/magic/global/desmos.d.ts +9 -0
- package/dist/magic/global/desmos.d.ts.map +1 -0
- package/dist/magic/global/desmos.js +11 -0
- package/dist/magic/global/desmos.js.map +1 -0
- package/dist/magic/global/index.d.ts +3 -0
- package/dist/magic/global/index.d.ts.map +1 -0
- package/dist/magic/global/index.js +10 -0
- package/dist/magic/global/index.js.map +1 -0
- package/dist/magic/global/viewport.d.ts +16 -0
- package/dist/magic/global/viewport.d.ts.map +1 -0
- package/dist/magic/global/viewport.js +12 -0
- package/dist/magic/global/viewport.js.map +1 -0
- package/dist/magic/incantation.d.ts +74 -0
- package/dist/magic/incantation.d.ts.map +1 -0
- package/dist/magic/incantation.js +81 -0
- package/dist/magic/incantation.js.map +1 -0
- package/dist/magic/index.d.ts +6 -0
- package/dist/magic/index.d.ts.map +1 -0
- package/dist/magic/index.js +5 -0
- package/dist/magic/index.js.map +1 -0
- package/dist/magic/local/anim.d.ts +7 -0
- package/dist/magic/local/anim.d.ts.map +1 -0
- package/dist/magic/local/anim.js +11 -0
- package/dist/magic/local/anim.js.map +1 -0
- package/dist/magic/local/colour.d.ts +12 -0
- package/dist/magic/local/colour.d.ts.map +1 -0
- package/dist/magic/local/colour.js +27 -0
- package/dist/magic/local/colour.js.map +1 -0
- package/dist/magic/local/dashed.d.ts +7 -0
- package/dist/magic/local/dashed.d.ts.map +1 -0
- package/dist/magic/local/dashed.js +10 -0
- package/dist/magic/local/dashed.js.map +1 -0
- package/dist/magic/local/fill.d.ts +13 -0
- package/dist/magic/local/fill.d.ts.map +1 -0
- package/dist/magic/local/fill.js +12 -0
- package/dist/magic/local/fill.js.map +1 -0
- package/dist/magic/local/hide.d.ts +7 -0
- package/dist/magic/local/hide.d.ts.map +1 -0
- package/dist/magic/local/hide.js +10 -0
- package/dist/magic/local/hide.js.map +1 -0
- package/dist/magic/local/index.d.ts +3 -0
- package/dist/magic/local/index.d.ts.map +1 -0
- package/dist/magic/local/index.js +24 -0
- package/dist/magic/local/index.js.map +1 -0
- package/dist/magic/local/line.d.ts +15 -0
- package/dist/magic/local/line.d.ts.map +1 -0
- package/dist/magic/local/line.js +17 -0
- package/dist/magic/local/line.js.map +1 -0
- package/dist/magic/local/no-line.d.ts +7 -0
- package/dist/magic/local/no-line.d.ts.map +1 -0
- package/dist/magic/local/no-line.js +10 -0
- package/dist/magic/local/no-line.js.map +1 -0
- package/dist/magic/local/point.d.ts +15 -0
- package/dist/magic/local/point.d.ts.map +1 -0
- package/dist/magic/local/point.js +17 -0
- package/dist/magic/local/point.js.map +1 -0
- package/dist/magic/local/secret.d.ts +7 -0
- package/dist/magic/local/secret.d.ts.map +1 -0
- package/dist/magic/local/secret.js +10 -0
- package/dist/magic/local/secret.js.map +1 -0
- package/dist/magic/local/slider.d.ts +15 -0
- package/dist/magic/local/slider.d.ts.map +1 -0
- package/dist/magic/local/slider.js +13 -0
- package/dist/magic/local/slider.js.map +1 -0
- package/dist/parser/ast.d.ts +40 -0
- package/dist/parser/ast.d.ts.map +1 -0
- package/dist/parser/ast.js +13 -0
- package/dist/parser/ast.js.map +1 -0
- package/dist/parser/desmost-parser.d.ts +105 -0
- package/dist/parser/desmost-parser.d.ts.map +1 -0
- package/dist/parser/desmost-parser.js +379 -0
- package/dist/parser/desmost-parser.js.map +1 -0
- package/dist/parser/generic-parser.d.ts +62 -0
- package/dist/parser/generic-parser.d.ts.map +1 -0
- package/dist/parser/generic-parser.js +143 -0
- package/dist/parser/generic-parser.js.map +1 -0
- package/dist/parser/index.d.ts +8 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +8 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/src/compiler/compile.d.ts +32 -0
- package/dist/src/compiler/compile.d.ts.map +1 -0
- package/dist/src/compiler/compile.js +101 -0
- package/dist/src/compiler/compile.js.map +1 -0
- package/dist/src/compiler/evaluate.d.ts +18 -0
- package/dist/src/compiler/evaluate.d.ts.map +1 -0
- package/dist/src/compiler/evaluate.js +83 -0
- package/dist/src/compiler/evaluate.js.map +1 -0
- package/dist/src/compiler/index.d.ts +3 -0
- package/dist/src/compiler/index.d.ts.map +1 -0
- package/dist/src/compiler/index.js +2 -0
- package/dist/src/compiler/index.js.map +1 -0
- package/dist/src/compiler/options.d.ts +54 -0
- package/dist/src/compiler/options.d.ts.map +1 -0
- package/dist/src/compiler/options.js +15 -0
- package/dist/src/compiler/options.js.map +1 -0
- package/dist/src/desmos.d.ts +12 -0
- package/dist/src/desmos.d.ts.map +1 -0
- package/dist/src/desmos.js +13 -0
- package/dist/src/desmos.js.map +1 -0
- package/dist/src/errors.d.ts +45 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +38 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/magic/expr/index.d.ts +3 -0
- package/dist/src/magic/expr/index.d.ts.map +1 -0
- package/dist/src/magic/expr/index.js +8 -0
- package/dist/src/magic/expr/index.js.map +1 -0
- package/dist/src/magic/expr/latex.d.ts +10 -0
- package/dist/src/magic/expr/latex.d.ts.map +1 -0
- package/dist/src/magic/expr/latex.js +16 -0
- package/dist/src/magic/expr/latex.js.map +1 -0
- package/dist/src/magic/expr/text.d.ts +9 -0
- package/dist/src/magic/expr/text.d.ts.map +1 -0
- package/dist/src/magic/expr/text.js +13 -0
- package/dist/src/magic/expr/text.js.map +1 -0
- package/dist/src/magic/global/dark-mode.d.ts +7 -0
- package/dist/src/magic/global/dark-mode.d.ts.map +1 -0
- package/dist/src/magic/global/dark-mode.js +11 -0
- package/dist/src/magic/global/dark-mode.js.map +1 -0
- package/dist/src/magic/global/desmos.d.ts +9 -0
- package/dist/src/magic/global/desmos.d.ts.map +1 -0
- package/dist/src/magic/global/desmos.js +11 -0
- package/dist/src/magic/global/desmos.js.map +1 -0
- package/dist/src/magic/global/index.d.ts +3 -0
- package/dist/src/magic/global/index.d.ts.map +1 -0
- package/dist/src/magic/global/index.js +10 -0
- package/dist/src/magic/global/index.js.map +1 -0
- package/dist/src/magic/global/viewport.d.ts +16 -0
- package/dist/src/magic/global/viewport.d.ts.map +1 -0
- package/dist/src/magic/global/viewport.js +12 -0
- package/dist/src/magic/global/viewport.js.map +1 -0
- package/dist/src/magic/incantation.d.ts +74 -0
- package/dist/src/magic/incantation.d.ts.map +1 -0
- package/dist/src/magic/incantation.js +81 -0
- package/dist/src/magic/incantation.js.map +1 -0
- package/dist/src/magic/index.d.ts +6 -0
- package/dist/src/magic/index.d.ts.map +1 -0
- package/dist/src/magic/index.js +5 -0
- package/dist/src/magic/index.js.map +1 -0
- package/dist/src/magic/local/anim.d.ts +7 -0
- package/dist/src/magic/local/anim.d.ts.map +1 -0
- package/dist/src/magic/local/anim.js +11 -0
- package/dist/src/magic/local/anim.js.map +1 -0
- package/dist/src/magic/local/colour.d.ts +12 -0
- package/dist/src/magic/local/colour.d.ts.map +1 -0
- package/dist/src/magic/local/colour.js +27 -0
- package/dist/src/magic/local/colour.js.map +1 -0
- package/dist/src/magic/local/dashed.d.ts +7 -0
- package/dist/src/magic/local/dashed.d.ts.map +1 -0
- package/dist/src/magic/local/dashed.js +10 -0
- package/dist/src/magic/local/dashed.js.map +1 -0
- package/dist/src/magic/local/fill.d.ts +13 -0
- package/dist/src/magic/local/fill.d.ts.map +1 -0
- package/dist/src/magic/local/fill.js +12 -0
- package/dist/src/magic/local/fill.js.map +1 -0
- package/dist/src/magic/local/hide.d.ts +7 -0
- package/dist/src/magic/local/hide.d.ts.map +1 -0
- package/dist/src/magic/local/hide.js +10 -0
- package/dist/src/magic/local/hide.js.map +1 -0
- package/dist/src/magic/local/index.d.ts +3 -0
- package/dist/src/magic/local/index.d.ts.map +1 -0
- package/dist/src/magic/local/index.js +24 -0
- package/dist/src/magic/local/index.js.map +1 -0
- package/dist/src/magic/local/line.d.ts +15 -0
- package/dist/src/magic/local/line.d.ts.map +1 -0
- package/dist/src/magic/local/line.js +17 -0
- package/dist/src/magic/local/line.js.map +1 -0
- package/dist/src/magic/local/no-line.d.ts +7 -0
- package/dist/src/magic/local/no-line.d.ts.map +1 -0
- package/dist/src/magic/local/no-line.js +10 -0
- package/dist/src/magic/local/no-line.js.map +1 -0
- package/dist/src/magic/local/point.d.ts +15 -0
- package/dist/src/magic/local/point.d.ts.map +1 -0
- package/dist/src/magic/local/point.js +17 -0
- package/dist/src/magic/local/point.js.map +1 -0
- package/dist/src/magic/local/secret.d.ts +7 -0
- package/dist/src/magic/local/secret.d.ts.map +1 -0
- package/dist/src/magic/local/secret.js +10 -0
- package/dist/src/magic/local/secret.js.map +1 -0
- package/dist/src/magic/local/slider.d.ts +15 -0
- package/dist/src/magic/local/slider.d.ts.map +1 -0
- package/dist/src/magic/local/slider.js +13 -0
- package/dist/src/magic/local/slider.js.map +1 -0
- package/dist/src/parser/ast.d.ts +40 -0
- package/dist/src/parser/ast.d.ts.map +1 -0
- package/dist/src/parser/ast.js +13 -0
- package/dist/src/parser/ast.js.map +1 -0
- package/dist/src/parser/desmost-parser.d.ts +105 -0
- package/dist/src/parser/desmost-parser.d.ts.map +1 -0
- package/dist/src/parser/desmost-parser.js +379 -0
- package/dist/src/parser/desmost-parser.js.map +1 -0
- package/dist/src/parser/generic-parser.d.ts +62 -0
- package/dist/src/parser/generic-parser.d.ts.map +1 -0
- package/dist/src/parser/generic-parser.js +143 -0
- package/dist/src/parser/generic-parser.js.map +1 -0
- package/dist/src/parser/index.d.ts +8 -0
- package/dist/src/parser/index.d.ts.map +1 -0
- package/dist/src/parser/index.js +8 -0
- package/dist/src/parser/index.js.map +1 -0
- package/package.json +58 -0
- package/src/compiler/compile.ts +127 -0
- package/src/compiler/evaluate.ts +121 -0
- package/src/compiler/index.ts +2 -0
- package/src/compiler/options.ts +77 -0
- package/src/desmos.ts +13 -0
- package/src/errors.ts +46 -0
- package/src/index.ts +1 -0
- package/src/magic/.template.ts +22 -0
- package/src/magic/expr/index.ts +11 -0
- package/src/magic/expr/latex.ts +24 -0
- package/src/magic/expr/text.ts +19 -0
- package/src/magic/global/dark-mode.ts +17 -0
- package/src/magic/global/desmos.ts +20 -0
- package/src/magic/global/index.ts +13 -0
- package/src/magic/global/viewport.ts +25 -0
- package/src/magic/incantation.ts +142 -0
- package/src/magic/index.ts +6 -0
- package/src/magic/local/anim.ts +17 -0
- package/src/magic/local/colour.ts +38 -0
- package/src/magic/local/dashed.ts +16 -0
- package/src/magic/local/fill.ts +24 -0
- package/src/magic/local/hide.ts +16 -0
- package/src/magic/local/index.ts +27 -0
- package/src/magic/local/line.ts +29 -0
- package/src/magic/local/no-line.ts +16 -0
- package/src/magic/local/point.ts +29 -0
- package/src/magic/local/secret.ts +16 -0
- package/src/magic/local/slider.ts +27 -0
- package/src/parser/ast.ts +60 -0
- package/src/parser/desmost-parser.ts +486 -0
- package/src/parser/generic-parser.ts +191 -0
- package/src/parser/index.ts +8 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { GenericParser } from "./generic-parser";
|
|
2
|
+
import { Ast } from "./ast";
|
|
3
|
+
|
|
4
|
+
import { RecoverableFail, UnrecoverableError } from "../errors";
|
|
5
|
+
import type { Recoverable, Unrecoverable, MaybeRecoverable } from "../errors";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
Incantation, ArgIncantation,
|
|
9
|
+
GLOBAL_INCANTATIONS, LOCAL_INCANTATIONS, EXPR_INCANTATIONS
|
|
10
|
+
} from "../magic";
|
|
11
|
+
import type { GLOBAL, LOCAL, EXPR } from "../magic";
|
|
12
|
+
|
|
13
|
+
import type { DesmostOptions } from "../compiler";
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A stateful lazy parser for Desmost.
|
|
18
|
+
*/
|
|
19
|
+
export class DesmostParser extends GenericParser
|
|
20
|
+
{
|
|
21
|
+
constructor(source: string, options?: DesmostOptions)
|
|
22
|
+
{
|
|
23
|
+
super(options?.ignore_trailing_blanks ? source : (source + "\n"));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
// == TOP-LEVEL == //
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse the next semantic block of source code, producing an `Ast` object, or `null` if the parser has successfully reached the end of the source code.
|
|
31
|
+
*/
|
|
32
|
+
public parse_next(): Ast | null
|
|
33
|
+
{
|
|
34
|
+
this.consume_spaces();
|
|
35
|
+
|
|
36
|
+
if (this.out_of_bounds()) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (this.current === "/") {
|
|
41
|
+
let r = this.parse_pre_sep();
|
|
42
|
+
|
|
43
|
+
if ("global" in r) {
|
|
44
|
+
// 1 global
|
|
45
|
+
return r.global;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 1+ locals + 1 expr
|
|
49
|
+
let incantations = r.local;
|
|
50
|
+
|
|
51
|
+
var expr = this.parse_post_sep();
|
|
52
|
+
|
|
53
|
+
for (let invocation of incantations) {
|
|
54
|
+
expr.incantations.push(invocation);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// 1 expr
|
|
59
|
+
var expr = this.parse_post_sep();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.consume_end_of_block();
|
|
63
|
+
return expr;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse Desmost syntax before the `::` separator, which may be:
|
|
68
|
+
*
|
|
69
|
+
* - 1 global incantation
|
|
70
|
+
* - 1+ local incantations
|
|
71
|
+
*/
|
|
72
|
+
parse_pre_sep():
|
|
73
|
+
Unrecoverable<
|
|
74
|
+
| { global: Ast.IncantationInvocation<GLOBAL>
|
|
75
|
+
| Ast.InvalidInvocation;
|
|
76
|
+
}
|
|
77
|
+
| { local: Array<Ast.IncantationInvocation<LOCAL>
|
|
78
|
+
| Ast.InvalidInvocation>;
|
|
79
|
+
}
|
|
80
|
+
>
|
|
81
|
+
{
|
|
82
|
+
// 1 global incantation
|
|
83
|
+
try {
|
|
84
|
+
let r = this.try_parse_global_incantation();
|
|
85
|
+
|
|
86
|
+
this.consume_end_of_block(
|
|
87
|
+
`Received excess input after global incantation /${r.incantation.identifier}`
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return { global: r };
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
if (!(e instanceof RecoverableFail)) throw e;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let incantations = [];
|
|
97
|
+
|
|
98
|
+
// 1+ local incantations
|
|
99
|
+
while (this.current === "/") {
|
|
100
|
+
try {
|
|
101
|
+
incantations.push(this.try_parse_local_incantation());
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
if (!(e instanceof RecoverableFail)) throw e;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (incantations.length > 0) {
|
|
110
|
+
this.parse_sep();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { local: incantations };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parse the Desmost `::` separator.
|
|
118
|
+
*/
|
|
119
|
+
parse_sep(): Unrecoverable<void>
|
|
120
|
+
{
|
|
121
|
+
this.consume_whitespace();
|
|
122
|
+
this.consume("::",
|
|
123
|
+
`Expected \`::\` separator between local incantations and expression, but found: \`${this.preview()}\``
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Parse Desmost syntax after the `::` separator, which may be:
|
|
129
|
+
*
|
|
130
|
+
* - 1 line of LaTeX
|
|
131
|
+
* - 1 expression incantation
|
|
132
|
+
*/
|
|
133
|
+
parse_post_sep(): Unrecoverable<Ast.Expression>
|
|
134
|
+
{
|
|
135
|
+
this.consume_spaces();
|
|
136
|
+
|
|
137
|
+
switch (this.current) {
|
|
138
|
+
// empty block
|
|
139
|
+
case "\n":
|
|
140
|
+
return {
|
|
141
|
+
kind: Ast.Kind.EXPRESSION,
|
|
142
|
+
data: { latex: ` ` },
|
|
143
|
+
incantations: [],
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// expr incantation
|
|
147
|
+
case "/":
|
|
148
|
+
try {
|
|
149
|
+
return this.try_parse_expr_incantation();
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
if (!(e instanceof RecoverableFail)) throw e;
|
|
153
|
+
// plain LaTeX (fallback)
|
|
154
|
+
return this.parse_latex_line();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// plain LaTeX
|
|
158
|
+
default:
|
|
159
|
+
return this.parse_latex_line();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
// == LOW-LEVEL == //
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Parse a single line of LaTeX.
|
|
168
|
+
*/
|
|
169
|
+
parse_latex_line(): Unrecoverable<Ast.Expression>
|
|
170
|
+
{
|
|
171
|
+
let init = this.i;
|
|
172
|
+
|
|
173
|
+
while (!this.out_of_bounds() && this.current !== "\n") {
|
|
174
|
+
this.advance();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
kind: Ast.Kind.EXPRESSION,
|
|
179
|
+
data: { latex: this.source.slice(init, this.i) },
|
|
180
|
+
incantations: [],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Attempt to parse a global incantation invocation.
|
|
187
|
+
*/
|
|
188
|
+
try_parse_global_incantation():
|
|
189
|
+
Recoverable<
|
|
190
|
+
| Ast.IncantationInvocation<GLOBAL>
|
|
191
|
+
| Ast.InvalidInvocation
|
|
192
|
+
>
|
|
193
|
+
{
|
|
194
|
+
let init = this.i;
|
|
195
|
+
|
|
196
|
+
this.try_consume("/");
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
var incantation = this.try_parse_identifier(GLOBAL_INCANTATIONS);
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
if (e instanceof RecoverableFail) {
|
|
203
|
+
this.i = init;
|
|
204
|
+
}
|
|
205
|
+
throw e;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let data = undefined;
|
|
209
|
+
|
|
210
|
+
if (incantation instanceof ArgIncantation) {
|
|
211
|
+
if (this.current === "{") {
|
|
212
|
+
data = this.parse_incantation_arg(incantation.arg_type);
|
|
213
|
+
}
|
|
214
|
+
else if (incantation.requires_arg) {
|
|
215
|
+
return {
|
|
216
|
+
kind: Ast.Kind.INVALID_INCANTATION,
|
|
217
|
+
incantation,
|
|
218
|
+
error: new UnrecoverableError.MissingInput(
|
|
219
|
+
`No argument provided for /${incantation.identifier}, which requires an argument of type: \`${incantation.arg_type}\``
|
|
220
|
+
)
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.consume_spaces();
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
kind: Ast.Kind.INCANTATION_INVOCATION,
|
|
229
|
+
incantation,
|
|
230
|
+
arg_raw: data,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Attempt to parse a local incantation invocation.
|
|
236
|
+
*/
|
|
237
|
+
try_parse_local_incantation():
|
|
238
|
+
Recoverable<
|
|
239
|
+
| Ast.IncantationInvocation<LOCAL>
|
|
240
|
+
| Ast.InvalidInvocation
|
|
241
|
+
>
|
|
242
|
+
{
|
|
243
|
+
let init = this.i;
|
|
244
|
+
|
|
245
|
+
this.try_consume("/");
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
var incantation = this.try_parse_identifier(LOCAL_INCANTATIONS);
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
if (e instanceof RecoverableFail) {
|
|
252
|
+
this.i = init;
|
|
253
|
+
}
|
|
254
|
+
throw e;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let arg_raw = undefined;
|
|
258
|
+
|
|
259
|
+
if (incantation instanceof ArgIncantation) {
|
|
260
|
+
if (this.current === "{") {
|
|
261
|
+
arg_raw = this.parse_incantation_arg(incantation.arg_type);
|
|
262
|
+
}
|
|
263
|
+
else if (incantation.requires_arg) {
|
|
264
|
+
return {
|
|
265
|
+
kind: Ast.Kind.INVALID_INCANTATION,
|
|
266
|
+
incantation,
|
|
267
|
+
error: new UnrecoverableError.MissingInput(
|
|
268
|
+
`No argument provided for /${incantation.identifier}, which requires an argument of type: \`${incantation.arg_type}\``
|
|
269
|
+
)
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
this.consume_whitespace();
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
kind: Ast.Kind.INCANTATION_INVOCATION,
|
|
278
|
+
incantation,
|
|
279
|
+
arg_raw,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
try_parse_expr_incantation(): MaybeRecoverable<Ast.Expression>
|
|
284
|
+
{
|
|
285
|
+
// TODO refactor with `backtrack()` helper
|
|
286
|
+
let init = this.i;
|
|
287
|
+
|
|
288
|
+
this.try_consume("/");
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
var incantation = this.try_parse_identifier(EXPR_INCANTATIONS) as ArgIncantation<EXPR>;
|
|
292
|
+
}
|
|
293
|
+
catch (e) {
|
|
294
|
+
if (e instanceof RecoverableFail) {
|
|
295
|
+
// TODO maybe flag to user
|
|
296
|
+
this.i = init;
|
|
297
|
+
}
|
|
298
|
+
throw e;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let arg_raw = this.parse_incantation_arg(incantation.arg_type);
|
|
302
|
+
|
|
303
|
+
let data = {};
|
|
304
|
+
incantation.apply(data, arg_raw);
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
kind: Ast.Kind.EXPRESSION,
|
|
308
|
+
data,
|
|
309
|
+
incantations: [],
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Attempt to parse an incantation identifier.
|
|
315
|
+
*/
|
|
316
|
+
try_parse_identifier<Effect extends Incantation.Effect>(
|
|
317
|
+
incantations: Incantation<Effect>[]
|
|
318
|
+
): Recoverable<Incantation<Effect>>
|
|
319
|
+
{
|
|
320
|
+
for (let incantation of incantations) {
|
|
321
|
+
// yeah the duplication here is a little meh, unfortunately needing `return` means we can't extract it into a helper
|
|
322
|
+
try {
|
|
323
|
+
this.try_consume(incantation.identifier);
|
|
324
|
+
return incantation;
|
|
325
|
+
}
|
|
326
|
+
catch (e) {
|
|
327
|
+
if (!(e instanceof RecoverableFail)) throw e;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (incantation.alias != undefined) {
|
|
331
|
+
try {
|
|
332
|
+
this.try_consume(incantation.alias);
|
|
333
|
+
return incantation;
|
|
334
|
+
}
|
|
335
|
+
catch (e) {
|
|
336
|
+
if (!(e instanceof RecoverableFail)) throw e;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
throw new RecoverableFail();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Parse an argument to an incantation, enclosed in `{}`.
|
|
346
|
+
*
|
|
347
|
+
* ```ts
|
|
348
|
+
* /incantation{ arg }
|
|
349
|
+
* ^^^^^^^
|
|
350
|
+
* ```
|
|
351
|
+
*
|
|
352
|
+
* ## Notes
|
|
353
|
+
*
|
|
354
|
+
* 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 `}`.
|
|
355
|
+
*
|
|
356
|
+
* However, the argument itself might be *meant* to contain a `}`. So we need a way to accurately identify the actual closing brace:
|
|
357
|
+
*
|
|
358
|
+
* ```ts
|
|
359
|
+
* /incantation{ field: { value: 1 } }
|
|
360
|
+
* ^ ^
|
|
361
|
+
* ```
|
|
362
|
+
*
|
|
363
|
+
* 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.
|
|
364
|
+
*
|
|
365
|
+
* However, there's still more edge cases:
|
|
366
|
+
*
|
|
367
|
+
* ```ts
|
|
368
|
+
* /label{ text: "This }s weird" }
|
|
369
|
+
* ^
|
|
370
|
+
* ```
|
|
371
|
+
*
|
|
372
|
+
* `{` and `}` can appear in strings, and they won't necessarily be matched, so we'll ignore them by also tracking string contexts.
|
|
373
|
+
*
|
|
374
|
+
* ```ts
|
|
375
|
+
* /latex{ \{ x, y ) }
|
|
376
|
+
* ^
|
|
377
|
+
* ```
|
|
378
|
+
*
|
|
379
|
+
* `{` 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.
|
|
380
|
+
*
|
|
381
|
+
* If the user truly mismatches `{}`, then, well ...parsing will fail catastrophically!
|
|
382
|
+
*/
|
|
383
|
+
parse_incantation_arg(
|
|
384
|
+
/**
|
|
385
|
+
* The type of argument to parse.
|
|
386
|
+
*
|
|
387
|
+
* Parsing strategy varies for different types:
|
|
388
|
+
*
|
|
389
|
+
* - String / LaTeX: Track only balanced `{}` and escaped `\{\}`.
|
|
390
|
+
* - JavaScript object: The above, plus balanced string quotes.
|
|
391
|
+
*/
|
|
392
|
+
arg_type: Incantation.ArgType,
|
|
393
|
+
): Unrecoverable<string>
|
|
394
|
+
{
|
|
395
|
+
let init = this.i;
|
|
396
|
+
|
|
397
|
+
this.consume("{",
|
|
398
|
+
`Expected \`{\` to start incantation argument, but found: \`${this.preview()}\``
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
enum Ctx {
|
|
402
|
+
BLOCK = Char.L_BRACE,
|
|
403
|
+
STR_1 = Char.QUOTE_SINGLE,
|
|
404
|
+
STR_2 = Char.QUOTE_DOUBLE,
|
|
405
|
+
STR_F = Char.BACKTICK,
|
|
406
|
+
ESCAPE = Char.BACKSLASH,
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let stack: Ctx[] = [Ctx.BLOCK];
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Pop `ctx` if it is the currently active context, returning `true` if successful.
|
|
413
|
+
*
|
|
414
|
+
* If `{ force: true }`, backtrack the stack and report errors for unterminated contexts.
|
|
415
|
+
*/
|
|
416
|
+
function try_pop(ctx: Ctx, options?: { force: boolean }): boolean
|
|
417
|
+
{
|
|
418
|
+
if (stack.at(-1) === ctx) {
|
|
419
|
+
stack.pop();
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
else if (options?.force) {
|
|
423
|
+
if (stack.includes(ctx)) {
|
|
424
|
+
/* NOTE: We could report errors for unterminated contexts, but we'll leave that for the actual evaluation - in case we get something wrong ;) */
|
|
425
|
+
while (stack.at(-1) !== ctx) {
|
|
426
|
+
stack.pop();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
while (stack.length > 0) {
|
|
437
|
+
if (try_pop(Ctx.ESCAPE)) {
|
|
438
|
+
this.advance(
|
|
439
|
+
`Unexpected end of input while parsing incantation argument, stack: ${JSON.stringify(stack)}`
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
let top = stack.at(-1)!;
|
|
444
|
+
|
|
445
|
+
switch (this.current) {
|
|
446
|
+
case Ctx.ESCAPE: stack.push(Ctx.ESCAPE); break;
|
|
447
|
+
|
|
448
|
+
/* NOTE: *Currently* `{}` should be ignored in all contexts except `Ctx.BLOCK`. This might change if more contexts are added in future! */
|
|
449
|
+
case Char.L_BRACE:
|
|
450
|
+
if (top !== Ctx.BLOCK) break;
|
|
451
|
+
stack.push(Ctx.BLOCK); break;
|
|
452
|
+
|
|
453
|
+
case Char.R_BRACE:
|
|
454
|
+
if (top !== Ctx.BLOCK) break;
|
|
455
|
+
try_pop(Ctx.BLOCK, { force: true }); break;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (arg_type === Incantation.ArgType.OBJECT) {
|
|
459
|
+
switch (this.current) {
|
|
460
|
+
case Ctx.STR_1: try_pop(Ctx.STR_1) || stack.push(Ctx.STR_1); break;
|
|
461
|
+
case Ctx.STR_2: try_pop(Ctx.STR_2) || stack.push(Ctx.STR_2); break;
|
|
462
|
+
case Ctx.STR_F: try_pop(Ctx.STR_F) || stack.push(Ctx.STR_F); break;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
this.advance(
|
|
467
|
+
`Unexpected end of input while parsing incantation argument, stack: ${JSON.stringify(stack)}`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* NOTE: Cut in by 1 on both sides to exclude {} braces */
|
|
472
|
+
return this.source.slice(init + 1, this.i - 1).trim();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
enum Char
|
|
479
|
+
{
|
|
480
|
+
L_BRACE = "{",
|
|
481
|
+
R_BRACE = "}",
|
|
482
|
+
QUOTE_SINGLE = `'`,
|
|
483
|
+
QUOTE_DOUBLE = `"`,
|
|
484
|
+
BACKTICK = "`",
|
|
485
|
+
BACKSLASH = "\\",
|
|
486
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { RecoverableFail, UnrecoverableError } from "../errors";
|
|
2
|
+
import type { Recoverable, Unrecoverable } from "../errors";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const IGNORED_CHARACTERS = new Set([
|
|
6
|
+
"\r",
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A stateful lazy parser, providing core methods for parsing.
|
|
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`.
|
|
14
|
+
*/
|
|
15
|
+
export class GenericParser
|
|
16
|
+
{
|
|
17
|
+
/** The source code this parser is parsing. */
|
|
18
|
+
protected source: string;
|
|
19
|
+
|
|
20
|
+
/** The current position in the source code the parser is pointing to. */
|
|
21
|
+
protected i: number = 0;
|
|
22
|
+
|
|
23
|
+
/** The number of characters in the source code. */
|
|
24
|
+
protected length: number;
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
/** Create a parser for parsing `source`. */
|
|
28
|
+
constructor(source: string)
|
|
29
|
+
{
|
|
30
|
+
this.source = source;
|
|
31
|
+
this.length = source.length;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Is the parser currently pointing at an invalid character?
|
|
37
|
+
*/
|
|
38
|
+
protected out_of_bounds(): boolean
|
|
39
|
+
{
|
|
40
|
+
return this.i >= this.length;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the character the parser is currently pointing at, or `undefined` if the parser is out-of-bounds.
|
|
45
|
+
*/
|
|
46
|
+
protected get current(): string | undefined
|
|
47
|
+
{
|
|
48
|
+
return this.source.at(this.i);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the character *after* the current one that the parser points at.
|
|
53
|
+
*/
|
|
54
|
+
protected next(): string | undefined
|
|
55
|
+
{
|
|
56
|
+
return this.source.at(this.i + 1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Peek a snippet of the upcoming source text (for error messages).
|
|
61
|
+
*/
|
|
62
|
+
public preview(): string
|
|
63
|
+
{
|
|
64
|
+
return this.source.slice(this.i, this.i + 20) + "...";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Advance to the next character, skipping ignored characters.
|
|
69
|
+
*
|
|
70
|
+
* Errors if the parser is the out-of-bounds *before* advancing.
|
|
71
|
+
*/
|
|
72
|
+
protected advance(error_msg?: string): Unrecoverable<void>
|
|
73
|
+
{
|
|
74
|
+
if (this.out_of_bounds()) {
|
|
75
|
+
throw new UnrecoverableError.UnexpectedEnd(error_msg ?? "Unexpected end of input");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.i++;
|
|
79
|
+
|
|
80
|
+
// @ts-expect-error: `this.current == undefined` is a true negative
|
|
81
|
+
if (IGNORED_CHARACTERS.has(this.current)) {
|
|
82
|
+
this.advance();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Attempt to advance to the next character, skipping ignored characters.
|
|
88
|
+
*/
|
|
89
|
+
protected try_advance(): Recoverable<void>
|
|
90
|
+
{
|
|
91
|
+
try {
|
|
92
|
+
this.advance();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
throw new RecoverableFail();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Consume `raw`, erroring if `raw` was not found.
|
|
101
|
+
*/
|
|
102
|
+
protected consume(raw: string, error_msg?: string): Unrecoverable<void>
|
|
103
|
+
{
|
|
104
|
+
if (this.out_of_bounds()) {
|
|
105
|
+
throw new UnrecoverableError.UnexpectedEnd(
|
|
106
|
+
`Unexpected end of input while trying to consume: \`${raw}\``
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let ii = 0;
|
|
111
|
+
|
|
112
|
+
while (this.current === raw[ii]) {
|
|
113
|
+
this.advance(
|
|
114
|
+
`Unexpected end of input while trying to consume: \`${raw}\``
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
ii++;
|
|
118
|
+
if (ii === raw.length) return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
throw new UnrecoverableError.UnexpectedInput(
|
|
122
|
+
error_msg ?? `Expected: \`${raw}\`, but found: \`${this.preview()}\``
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Attempt to consume `raw`, backtracking and throwing if `raw` was not found.
|
|
128
|
+
*/
|
|
129
|
+
protected try_consume(raw: string): Recoverable<void>
|
|
130
|
+
{
|
|
131
|
+
let init = this.i;
|
|
132
|
+
let ii = 0;
|
|
133
|
+
|
|
134
|
+
while (this.current === raw[ii]) {
|
|
135
|
+
this.try_advance();
|
|
136
|
+
ii++;
|
|
137
|
+
|
|
138
|
+
if (ii === raw.length) return;
|
|
139
|
+
if (this.i === this.length) break;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this.i = init;
|
|
143
|
+
throw new RecoverableFail();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Consume 0 or more space characters.
|
|
148
|
+
*/
|
|
149
|
+
protected consume_spaces(): void
|
|
150
|
+
{
|
|
151
|
+
/* NOTE: Callers should be able to assume this is safe to call even when at end of input, since it should just match 0 characters */
|
|
152
|
+
if (this.out_of_bounds()) return;
|
|
153
|
+
|
|
154
|
+
while (this.current === " ") {
|
|
155
|
+
this.advance();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Consume 0 or more whitespace characters.
|
|
161
|
+
*
|
|
162
|
+
* Same as `.consume_spaces()`, but allows newlines.
|
|
163
|
+
*/
|
|
164
|
+
protected consume_whitespace(): void
|
|
165
|
+
{
|
|
166
|
+
/* NOTE: Callers should be able to assume this is safe to call even when at end of input, since it should just match 0 characters */
|
|
167
|
+
if (this.i >= this.length) return;
|
|
168
|
+
|
|
169
|
+
while (this.current === " " || this.current === "\n") {
|
|
170
|
+
this.advance();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
protected consume_end_of_block(error_msg?: string): Unrecoverable<void>
|
|
175
|
+
{
|
|
176
|
+
if (this.i >= this.length) return;
|
|
177
|
+
|
|
178
|
+
this.consume_spaces();
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
this.try_consume("\n");
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
if (!(e instanceof RecoverableFail)) throw e;
|
|
185
|
+
|
|
186
|
+
throw new UnrecoverableError.ExcessInput(
|
|
187
|
+
error_msg ?? `Expected end of block, but found: \`${this.preview()}\``
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
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
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { DesmostParser } from "./desmost-parser";
|
|
8
|
+
export { Ast } from "./ast";
|