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.
Files changed (281) hide show
  1. package/dist/compiler/compile.d.ts +32 -0
  2. package/dist/compiler/compile.d.ts.map +1 -0
  3. package/dist/compiler/compile.js +101 -0
  4. package/dist/compiler/compile.js.map +1 -0
  5. package/dist/compiler/evaluate.d.ts +18 -0
  6. package/dist/compiler/evaluate.d.ts.map +1 -0
  7. package/dist/compiler/evaluate.js +83 -0
  8. package/dist/compiler/evaluate.js.map +1 -0
  9. package/dist/compiler/index.d.ts +3 -0
  10. package/dist/compiler/index.d.ts.map +1 -0
  11. package/dist/compiler/index.js +2 -0
  12. package/dist/compiler/index.js.map +1 -0
  13. package/dist/compiler/options.d.ts +54 -0
  14. package/dist/compiler/options.d.ts.map +1 -0
  15. package/dist/compiler/options.js +15 -0
  16. package/dist/compiler/options.js.map +1 -0
  17. package/dist/desmos.d.ts +12 -0
  18. package/dist/desmos.d.ts.map +1 -0
  19. package/dist/desmos.js +13 -0
  20. package/dist/desmos.js.map +1 -0
  21. package/dist/errors.d.ts +45 -0
  22. package/dist/errors.d.ts.map +1 -0
  23. package/dist/errors.js +38 -0
  24. package/dist/errors.js.map +1 -0
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/magic/expr/index.d.ts +3 -0
  30. package/dist/magic/expr/index.d.ts.map +1 -0
  31. package/dist/magic/expr/index.js +8 -0
  32. package/dist/magic/expr/index.js.map +1 -0
  33. package/dist/magic/expr/latex.d.ts +10 -0
  34. package/dist/magic/expr/latex.d.ts.map +1 -0
  35. package/dist/magic/expr/latex.js +16 -0
  36. package/dist/magic/expr/latex.js.map +1 -0
  37. package/dist/magic/expr/text.d.ts +9 -0
  38. package/dist/magic/expr/text.d.ts.map +1 -0
  39. package/dist/magic/expr/text.js +13 -0
  40. package/dist/magic/expr/text.js.map +1 -0
  41. package/dist/magic/global/dark-mode.d.ts +7 -0
  42. package/dist/magic/global/dark-mode.d.ts.map +1 -0
  43. package/dist/magic/global/dark-mode.js +11 -0
  44. package/dist/magic/global/dark-mode.js.map +1 -0
  45. package/dist/magic/global/desmos.d.ts +9 -0
  46. package/dist/magic/global/desmos.d.ts.map +1 -0
  47. package/dist/magic/global/desmos.js +11 -0
  48. package/dist/magic/global/desmos.js.map +1 -0
  49. package/dist/magic/global/index.d.ts +3 -0
  50. package/dist/magic/global/index.d.ts.map +1 -0
  51. package/dist/magic/global/index.js +10 -0
  52. package/dist/magic/global/index.js.map +1 -0
  53. package/dist/magic/global/viewport.d.ts +16 -0
  54. package/dist/magic/global/viewport.d.ts.map +1 -0
  55. package/dist/magic/global/viewport.js +12 -0
  56. package/dist/magic/global/viewport.js.map +1 -0
  57. package/dist/magic/incantation.d.ts +74 -0
  58. package/dist/magic/incantation.d.ts.map +1 -0
  59. package/dist/magic/incantation.js +81 -0
  60. package/dist/magic/incantation.js.map +1 -0
  61. package/dist/magic/index.d.ts +6 -0
  62. package/dist/magic/index.d.ts.map +1 -0
  63. package/dist/magic/index.js +5 -0
  64. package/dist/magic/index.js.map +1 -0
  65. package/dist/magic/local/anim.d.ts +7 -0
  66. package/dist/magic/local/anim.d.ts.map +1 -0
  67. package/dist/magic/local/anim.js +11 -0
  68. package/dist/magic/local/anim.js.map +1 -0
  69. package/dist/magic/local/colour.d.ts +12 -0
  70. package/dist/magic/local/colour.d.ts.map +1 -0
  71. package/dist/magic/local/colour.js +27 -0
  72. package/dist/magic/local/colour.js.map +1 -0
  73. package/dist/magic/local/dashed.d.ts +7 -0
  74. package/dist/magic/local/dashed.d.ts.map +1 -0
  75. package/dist/magic/local/dashed.js +10 -0
  76. package/dist/magic/local/dashed.js.map +1 -0
  77. package/dist/magic/local/fill.d.ts +13 -0
  78. package/dist/magic/local/fill.d.ts.map +1 -0
  79. package/dist/magic/local/fill.js +12 -0
  80. package/dist/magic/local/fill.js.map +1 -0
  81. package/dist/magic/local/hide.d.ts +7 -0
  82. package/dist/magic/local/hide.d.ts.map +1 -0
  83. package/dist/magic/local/hide.js +10 -0
  84. package/dist/magic/local/hide.js.map +1 -0
  85. package/dist/magic/local/index.d.ts +3 -0
  86. package/dist/magic/local/index.d.ts.map +1 -0
  87. package/dist/magic/local/index.js +24 -0
  88. package/dist/magic/local/index.js.map +1 -0
  89. package/dist/magic/local/line.d.ts +15 -0
  90. package/dist/magic/local/line.d.ts.map +1 -0
  91. package/dist/magic/local/line.js +17 -0
  92. package/dist/magic/local/line.js.map +1 -0
  93. package/dist/magic/local/no-line.d.ts +7 -0
  94. package/dist/magic/local/no-line.d.ts.map +1 -0
  95. package/dist/magic/local/no-line.js +10 -0
  96. package/dist/magic/local/no-line.js.map +1 -0
  97. package/dist/magic/local/point.d.ts +15 -0
  98. package/dist/magic/local/point.d.ts.map +1 -0
  99. package/dist/magic/local/point.js +17 -0
  100. package/dist/magic/local/point.js.map +1 -0
  101. package/dist/magic/local/secret.d.ts +7 -0
  102. package/dist/magic/local/secret.d.ts.map +1 -0
  103. package/dist/magic/local/secret.js +10 -0
  104. package/dist/magic/local/secret.js.map +1 -0
  105. package/dist/magic/local/slider.d.ts +15 -0
  106. package/dist/magic/local/slider.d.ts.map +1 -0
  107. package/dist/magic/local/slider.js +13 -0
  108. package/dist/magic/local/slider.js.map +1 -0
  109. package/dist/parser/ast.d.ts +40 -0
  110. package/dist/parser/ast.d.ts.map +1 -0
  111. package/dist/parser/ast.js +13 -0
  112. package/dist/parser/ast.js.map +1 -0
  113. package/dist/parser/desmost-parser.d.ts +105 -0
  114. package/dist/parser/desmost-parser.d.ts.map +1 -0
  115. package/dist/parser/desmost-parser.js +379 -0
  116. package/dist/parser/desmost-parser.js.map +1 -0
  117. package/dist/parser/generic-parser.d.ts +62 -0
  118. package/dist/parser/generic-parser.d.ts.map +1 -0
  119. package/dist/parser/generic-parser.js +143 -0
  120. package/dist/parser/generic-parser.js.map +1 -0
  121. package/dist/parser/index.d.ts +8 -0
  122. package/dist/parser/index.d.ts.map +1 -0
  123. package/dist/parser/index.js +8 -0
  124. package/dist/parser/index.js.map +1 -0
  125. package/dist/src/compiler/compile.d.ts +32 -0
  126. package/dist/src/compiler/compile.d.ts.map +1 -0
  127. package/dist/src/compiler/compile.js +101 -0
  128. package/dist/src/compiler/compile.js.map +1 -0
  129. package/dist/src/compiler/evaluate.d.ts +18 -0
  130. package/dist/src/compiler/evaluate.d.ts.map +1 -0
  131. package/dist/src/compiler/evaluate.js +83 -0
  132. package/dist/src/compiler/evaluate.js.map +1 -0
  133. package/dist/src/compiler/index.d.ts +3 -0
  134. package/dist/src/compiler/index.d.ts.map +1 -0
  135. package/dist/src/compiler/index.js +2 -0
  136. package/dist/src/compiler/index.js.map +1 -0
  137. package/dist/src/compiler/options.d.ts +54 -0
  138. package/dist/src/compiler/options.d.ts.map +1 -0
  139. package/dist/src/compiler/options.js +15 -0
  140. package/dist/src/compiler/options.js.map +1 -0
  141. package/dist/src/desmos.d.ts +12 -0
  142. package/dist/src/desmos.d.ts.map +1 -0
  143. package/dist/src/desmos.js +13 -0
  144. package/dist/src/desmos.js.map +1 -0
  145. package/dist/src/errors.d.ts +45 -0
  146. package/dist/src/errors.d.ts.map +1 -0
  147. package/dist/src/errors.js +38 -0
  148. package/dist/src/errors.js.map +1 -0
  149. package/dist/src/index.d.ts +2 -0
  150. package/dist/src/index.d.ts.map +1 -0
  151. package/dist/src/index.js +2 -0
  152. package/dist/src/index.js.map +1 -0
  153. package/dist/src/magic/expr/index.d.ts +3 -0
  154. package/dist/src/magic/expr/index.d.ts.map +1 -0
  155. package/dist/src/magic/expr/index.js +8 -0
  156. package/dist/src/magic/expr/index.js.map +1 -0
  157. package/dist/src/magic/expr/latex.d.ts +10 -0
  158. package/dist/src/magic/expr/latex.d.ts.map +1 -0
  159. package/dist/src/magic/expr/latex.js +16 -0
  160. package/dist/src/magic/expr/latex.js.map +1 -0
  161. package/dist/src/magic/expr/text.d.ts +9 -0
  162. package/dist/src/magic/expr/text.d.ts.map +1 -0
  163. package/dist/src/magic/expr/text.js +13 -0
  164. package/dist/src/magic/expr/text.js.map +1 -0
  165. package/dist/src/magic/global/dark-mode.d.ts +7 -0
  166. package/dist/src/magic/global/dark-mode.d.ts.map +1 -0
  167. package/dist/src/magic/global/dark-mode.js +11 -0
  168. package/dist/src/magic/global/dark-mode.js.map +1 -0
  169. package/dist/src/magic/global/desmos.d.ts +9 -0
  170. package/dist/src/magic/global/desmos.d.ts.map +1 -0
  171. package/dist/src/magic/global/desmos.js +11 -0
  172. package/dist/src/magic/global/desmos.js.map +1 -0
  173. package/dist/src/magic/global/index.d.ts +3 -0
  174. package/dist/src/magic/global/index.d.ts.map +1 -0
  175. package/dist/src/magic/global/index.js +10 -0
  176. package/dist/src/magic/global/index.js.map +1 -0
  177. package/dist/src/magic/global/viewport.d.ts +16 -0
  178. package/dist/src/magic/global/viewport.d.ts.map +1 -0
  179. package/dist/src/magic/global/viewport.js +12 -0
  180. package/dist/src/magic/global/viewport.js.map +1 -0
  181. package/dist/src/magic/incantation.d.ts +74 -0
  182. package/dist/src/magic/incantation.d.ts.map +1 -0
  183. package/dist/src/magic/incantation.js +81 -0
  184. package/dist/src/magic/incantation.js.map +1 -0
  185. package/dist/src/magic/index.d.ts +6 -0
  186. package/dist/src/magic/index.d.ts.map +1 -0
  187. package/dist/src/magic/index.js +5 -0
  188. package/dist/src/magic/index.js.map +1 -0
  189. package/dist/src/magic/local/anim.d.ts +7 -0
  190. package/dist/src/magic/local/anim.d.ts.map +1 -0
  191. package/dist/src/magic/local/anim.js +11 -0
  192. package/dist/src/magic/local/anim.js.map +1 -0
  193. package/dist/src/magic/local/colour.d.ts +12 -0
  194. package/dist/src/magic/local/colour.d.ts.map +1 -0
  195. package/dist/src/magic/local/colour.js +27 -0
  196. package/dist/src/magic/local/colour.js.map +1 -0
  197. package/dist/src/magic/local/dashed.d.ts +7 -0
  198. package/dist/src/magic/local/dashed.d.ts.map +1 -0
  199. package/dist/src/magic/local/dashed.js +10 -0
  200. package/dist/src/magic/local/dashed.js.map +1 -0
  201. package/dist/src/magic/local/fill.d.ts +13 -0
  202. package/dist/src/magic/local/fill.d.ts.map +1 -0
  203. package/dist/src/magic/local/fill.js +12 -0
  204. package/dist/src/magic/local/fill.js.map +1 -0
  205. package/dist/src/magic/local/hide.d.ts +7 -0
  206. package/dist/src/magic/local/hide.d.ts.map +1 -0
  207. package/dist/src/magic/local/hide.js +10 -0
  208. package/dist/src/magic/local/hide.js.map +1 -0
  209. package/dist/src/magic/local/index.d.ts +3 -0
  210. package/dist/src/magic/local/index.d.ts.map +1 -0
  211. package/dist/src/magic/local/index.js +24 -0
  212. package/dist/src/magic/local/index.js.map +1 -0
  213. package/dist/src/magic/local/line.d.ts +15 -0
  214. package/dist/src/magic/local/line.d.ts.map +1 -0
  215. package/dist/src/magic/local/line.js +17 -0
  216. package/dist/src/magic/local/line.js.map +1 -0
  217. package/dist/src/magic/local/no-line.d.ts +7 -0
  218. package/dist/src/magic/local/no-line.d.ts.map +1 -0
  219. package/dist/src/magic/local/no-line.js +10 -0
  220. package/dist/src/magic/local/no-line.js.map +1 -0
  221. package/dist/src/magic/local/point.d.ts +15 -0
  222. package/dist/src/magic/local/point.d.ts.map +1 -0
  223. package/dist/src/magic/local/point.js +17 -0
  224. package/dist/src/magic/local/point.js.map +1 -0
  225. package/dist/src/magic/local/secret.d.ts +7 -0
  226. package/dist/src/magic/local/secret.d.ts.map +1 -0
  227. package/dist/src/magic/local/secret.js +10 -0
  228. package/dist/src/magic/local/secret.js.map +1 -0
  229. package/dist/src/magic/local/slider.d.ts +15 -0
  230. package/dist/src/magic/local/slider.d.ts.map +1 -0
  231. package/dist/src/magic/local/slider.js +13 -0
  232. package/dist/src/magic/local/slider.js.map +1 -0
  233. package/dist/src/parser/ast.d.ts +40 -0
  234. package/dist/src/parser/ast.d.ts.map +1 -0
  235. package/dist/src/parser/ast.js +13 -0
  236. package/dist/src/parser/ast.js.map +1 -0
  237. package/dist/src/parser/desmost-parser.d.ts +105 -0
  238. package/dist/src/parser/desmost-parser.d.ts.map +1 -0
  239. package/dist/src/parser/desmost-parser.js +379 -0
  240. package/dist/src/parser/desmost-parser.js.map +1 -0
  241. package/dist/src/parser/generic-parser.d.ts +62 -0
  242. package/dist/src/parser/generic-parser.d.ts.map +1 -0
  243. package/dist/src/parser/generic-parser.js +143 -0
  244. package/dist/src/parser/generic-parser.js.map +1 -0
  245. package/dist/src/parser/index.d.ts +8 -0
  246. package/dist/src/parser/index.d.ts.map +1 -0
  247. package/dist/src/parser/index.js +8 -0
  248. package/dist/src/parser/index.js.map +1 -0
  249. package/package.json +58 -0
  250. package/src/compiler/compile.ts +127 -0
  251. package/src/compiler/evaluate.ts +121 -0
  252. package/src/compiler/index.ts +2 -0
  253. package/src/compiler/options.ts +77 -0
  254. package/src/desmos.ts +13 -0
  255. package/src/errors.ts +46 -0
  256. package/src/index.ts +1 -0
  257. package/src/magic/.template.ts +22 -0
  258. package/src/magic/expr/index.ts +11 -0
  259. package/src/magic/expr/latex.ts +24 -0
  260. package/src/magic/expr/text.ts +19 -0
  261. package/src/magic/global/dark-mode.ts +17 -0
  262. package/src/magic/global/desmos.ts +20 -0
  263. package/src/magic/global/index.ts +13 -0
  264. package/src/magic/global/viewport.ts +25 -0
  265. package/src/magic/incantation.ts +142 -0
  266. package/src/magic/index.ts +6 -0
  267. package/src/magic/local/anim.ts +17 -0
  268. package/src/magic/local/colour.ts +38 -0
  269. package/src/magic/local/dashed.ts +16 -0
  270. package/src/magic/local/fill.ts +24 -0
  271. package/src/magic/local/hide.ts +16 -0
  272. package/src/magic/local/index.ts +27 -0
  273. package/src/magic/local/line.ts +29 -0
  274. package/src/magic/local/no-line.ts +16 -0
  275. package/src/magic/local/point.ts +29 -0
  276. package/src/magic/local/secret.ts +16 -0
  277. package/src/magic/local/slider.ts +27 -0
  278. package/src/parser/ast.ts +60 -0
  279. package/src/parser/desmost-parser.ts +486 -0
  280. package/src/parser/generic-parser.ts +191 -0
  281. 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";