A world-class math input for the web
at main 275 lines 8.0 kB view raw
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);