A world-class math input for the web
1import { EditorState } from "./editorState";
2import { CaretNode } from "./node";
3import { MissingNode } from "./nodes/missing";
4import { UnparseableNode } from "./nodes/unparseable";
5import {
6 CaretJuxtapositionParselet,
7 CaretInfixParselet,
8 CaretLeafParselet,
9 CaretParselet,
10 CaretPostfixParselet,
11 CaretPrefixParselet,
12 CannotParseError,
13} from "./parselet";
14import { StrandPath } from "./path";
15import { Strand } from "./strand";
16import { Token } from "./token";
17
18export class CaretParser {
19 private _position: number = 0;
20 private _editorState: EditorState;
21 private _strandPath: StrandPath;
22 private _parselets: CaretParselet[];
23 private _parseletBindingPower: Map<CaretParselet, number> = new Map();
24 // public activeFlags: Set<Symbol> = new Set();
25
26 constructor(
27 editorState: EditorState,
28 strandPath: StrandPath = [],
29 parselets: (CaretParselet | Set<CaretParselet>)[]
30 // activeFlags: Set<Symbol> = new Set()
31 ) {
32 this._editorState = editorState;
33 this._strandPath = strandPath;
34 // this.activeFlags = activeFlags;
35 this._parselets = parselets.flatMap((p) =>
36 p instanceof Set ? Array.from(p) : p
37 );
38 for (let i = 0; i < parselets.length; i++) {
39 const parseletOrSet = parselets[i];
40 const bindingPower = parselets.length - i;
41 if (parseletOrSet instanceof Set) {
42 for (const parselet of parseletOrSet) {
43 this._parseletBindingPower.set(parselet, bindingPower);
44 }
45 } else {
46 this._parseletBindingPower.set(parseletOrSet, bindingPower);
47 }
48 }
49 }
50
51 public get editorState() {
52 return this._editorState;
53 }
54
55 public get strand(): Strand {
56 const strand = this._editorState.content.getStrand(this._strandPath);
57 if (!strand) {
58 throw new Error(
59 `Strand not found at path: ${JSON.stringify(this._strandPath)}`
60 );
61 }
62 return strand;
63 }
64
65 public parse() {
66 return this.parseMinBP(0);
67 }
68
69 public parseMinBP(minBindingPower: number | CaretParselet): CaretNode {
70 if (typeof minBindingPower !== "number") {
71 minBindingPower = this._parseletBindingPower.get(minBindingPower) ?? 0;
72 }
73
74 if (this.done()) return new MissingNode();
75
76 const parselets = this.filterParselets(["leaf", "prefix"]);
77 let resultNode: CaretNode | undefined;
78 for (const parselet of parselets) {
79 const positionBefore = this._position;
80 try {
81 resultNode = parselet.parse(this);
82 break;
83 } catch (error) {
84 if (error instanceof CannotParseError) {
85 this._position = positionBefore;
86 continue;
87 }
88 throw error;
89 }
90 }
91 if (!resultNode) {
92 const unparsedTokens = this.strand.tokens.slice(this._position);
93 this._position = this.strand.tokens.length;
94 return UnparseableNode.from(null, unparsedTokens);
95 }
96
97 let left: CaretNode = resultNode;
98
99 while (!this.done()) {
100 let resultNode: CaretNode | undefined;
101
102 {
103 const parselets = this.filterParselets(["infix", "postfix"]);
104 for (const parselet of parselets) {
105 const bindingPower = this.getBindingPower(parselet);
106 if (bindingPower < minBindingPower) continue;
107 if (
108 bindingPower === minBindingPower &&
109 parselet instanceof CaretInfixParselet &&
110 parselet.associativity === "left"
111 ) {
112 continue;
113 }
114
115 const positionBefore = this._position;
116 try {
117 resultNode = parselet.parse(this, left);
118 break;
119 } catch (error) {
120 if (error instanceof CannotParseError) {
121 this._position = positionBefore;
122 continue;
123 }
124 throw error;
125 }
126 }
127 }
128
129 if (resultNode) {
130 left = resultNode;
131 continue;
132 }
133
134 {
135 const parselets = this.filterParselets(["juxtaposition"]);
136 for (const parselet of parselets) {
137 const bindingPower = this.getBindingPower(parselet);
138 if (bindingPower < minBindingPower) continue;
139
140 const positionBefore = this._position;
141 try {
142 const right = this.parseMinBP(bindingPower);
143 if (
144 !(left instanceof UnparseableNode) &&
145 !(right instanceof UnparseableNode) &&
146 parselet.canParse(left, right)
147 ) {
148 resultNode = parselet.parse(this, left, right);
149 break;
150 } else {
151 this._position = positionBefore;
152 }
153 } catch (error) {
154 if (error instanceof CannotParseError) {
155 this._position = positionBefore;
156 continue;
157 }
158 throw error;
159 }
160 }
161 }
162
163 if (resultNode) {
164 left = resultNode;
165 continue;
166 }
167
168 // No parselets matched; stop parsing here
169 break;
170 }
171
172 // Sometimes ending early is okay (if we are sub-parsing), but if we're at the root
173 // level and there are still tokens left, we should return an UnparseableNode
174 if (this.peek() !== null && minBindingPower === 0) {
175 return UnparseableNode.from(
176 resultNode,
177 this.strand.tokens.slice(this._position)
178 );
179 }
180
181 return left;
182 }
183
184 public parseSubStrand(
185 strandOrPath: StrandPath | Strand
186 // activeFlags: Set<Symbol> = this.activeFlags
187 ): CaretNode {
188 const path =
189 strandOrPath instanceof Strand
190 ? this.editorState.content.findStrandPath(strandOrPath)
191 : strandOrPath;
192
193 if (!path) {
194 throw new Error(
195 `Strand not found in editorState: ${JSON.stringify(strandOrPath)}`
196 );
197 }
198
199 let tokenAtPath: Token | null = null;
200
201 if (path.length > 0) {
202 tokenAtPath = this.editorState.content.getToken({
203 strandPath: path.slice(0, -1),
204 tokenIndex: path[path.length - 1].tokenIndex,
205 });
206 }
207
208 const subParser = new CaretParser(
209 this._editorState,
210 path,
211 this._parselets
212 // activeFlags
213 );
214 return subParser.parse();
215 }
216
217 public peek(): Token | null {
218 if (this._position < this.strand.tokens.length) {
219 return this.strand.tokens[this._position];
220 }
221 return null;
222 }
223
224 public consume(): Token | null {
225 if (this._position < this.strand.tokens.length) {
226 return this.strand.tokens[this._position++];
227 }
228 return null;
229 }
230
231 public done(): boolean {
232 return this._position >= this.strand.tokens.length;
233 }
234
235 private filterParselets<
236 T extends "leaf" | "prefix" | "infix" | "postfix" | "juxtaposition"
237 >(validTypes: T[]): ParseletType<T>[] {
238 return this._parselets.filter((parselet) =>
239 validTypes.some((typeName) => {
240 switch (typeName) {
241 case "leaf":
242 return parselet instanceof CaretLeafParselet;
243 case "prefix":
244 return parselet instanceof CaretPrefixParselet;
245 case "infix":
246 return parselet instanceof CaretInfixParselet;
247 case "postfix":
248 return parselet instanceof CaretPostfixParselet;
249 case "juxtaposition":
250 return parselet instanceof CaretJuxtapositionParselet;
251 default:
252 typeName satisfies never;
253 return false;
254 }
255 })
256 ) as ParseletType<T>[];
257 }
258
259 private getBindingPower(parselet: CaretParselet): number {
260 const bindingPower = this._parseletBindingPower.get(parselet);
261 if (bindingPower === undefined) {
262 throw new Error(`Parselet not registered: ${parselet.constructor.name}`);
263 }
264 return bindingPower;
265 }
266}
267
268type ParseletType<
269 T extends "leaf" | "prefix" | "infix" | "postfix" | "juxtaposition"
270> =
271 | (T extends "leaf" ? CaretLeafParselet : never)
272 | (T extends "prefix" ? CaretPrefixParselet : never)
273 | (T extends "infix" ? CaretInfixParselet : never)
274 | (T extends "postfix" ? CaretPostfixParselet : never)
275 | (T extends "juxtaposition" ? CaretJuxtapositionParselet : never);