import { EditorState } from "./editorState"; import { CaretNode } from "./node"; import { MissingNode } from "./nodes/missing"; import { UnparseableNode } from "./nodes/unparseable"; import { CaretJuxtapositionParselet, CaretInfixParselet, CaretLeafParselet, CaretParselet, CaretPostfixParselet, CaretPrefixParselet, CannotParseError, } from "./parselet"; import { StrandPath } from "./path"; import { Strand } from "./strand"; import { Token } from "./token"; export class CaretParser { private _position: number = 0; private _editorState: EditorState; private _strandPath: StrandPath; private _parselets: CaretParselet[]; private _parseletBindingPower: Map = new Map(); // public activeFlags: Set = new Set(); constructor( editorState: EditorState, strandPath: StrandPath = [], parselets: (CaretParselet | Set)[] // activeFlags: Set = new Set() ) { this._editorState = editorState; this._strandPath = strandPath; // this.activeFlags = activeFlags; this._parselets = parselets.flatMap((p) => p instanceof Set ? Array.from(p) : p ); for (let i = 0; i < parselets.length; i++) { const parseletOrSet = parselets[i]; const bindingPower = parselets.length - i; if (parseletOrSet instanceof Set) { for (const parselet of parseletOrSet) { this._parseletBindingPower.set(parselet, bindingPower); } } else { this._parseletBindingPower.set(parseletOrSet, bindingPower); } } } public get editorState() { return this._editorState; } public get strand(): Strand { const strand = this._editorState.content.getStrand(this._strandPath); if (!strand) { throw new Error( `Strand not found at path: ${JSON.stringify(this._strandPath)}` ); } return strand; } public parse() { return this.parseMinBP(0); } public parseMinBP(minBindingPower: number | CaretParselet): CaretNode { if (typeof minBindingPower !== "number") { minBindingPower = this._parseletBindingPower.get(minBindingPower) ?? 0; } if (this.done()) return new MissingNode(); const parselets = this.filterParselets(["leaf", "prefix"]); let resultNode: CaretNode | undefined; for (const parselet of parselets) { const positionBefore = this._position; try { resultNode = parselet.parse(this); break; } catch (error) { if (error instanceof CannotParseError) { this._position = positionBefore; continue; } throw error; } } if (!resultNode) { const unparsedTokens = this.strand.tokens.slice(this._position); this._position = this.strand.tokens.length; return UnparseableNode.from(null, unparsedTokens); } let left: CaretNode = resultNode; while (!this.done()) { let resultNode: CaretNode | undefined; { const parselets = this.filterParselets(["infix", "postfix"]); for (const parselet of parselets) { const bindingPower = this.getBindingPower(parselet); if (bindingPower < minBindingPower) continue; if ( bindingPower === minBindingPower && parselet instanceof CaretInfixParselet && parselet.associativity === "left" ) { continue; } const positionBefore = this._position; try { resultNode = parselet.parse(this, left); break; } catch (error) { if (error instanceof CannotParseError) { this._position = positionBefore; continue; } throw error; } } } if (resultNode) { left = resultNode; continue; } { const parselets = this.filterParselets(["juxtaposition"]); for (const parselet of parselets) { const bindingPower = this.getBindingPower(parselet); if (bindingPower < minBindingPower) continue; const positionBefore = this._position; try { const right = this.parseMinBP(bindingPower); if ( !(left instanceof UnparseableNode) && !(right instanceof UnparseableNode) && parselet.canParse(left, right) ) { resultNode = parselet.parse(this, left, right); break; } else { this._position = positionBefore; } } catch (error) { if (error instanceof CannotParseError) { this._position = positionBefore; continue; } throw error; } } } if (resultNode) { left = resultNode; continue; } // No parselets matched; stop parsing here break; } // Sometimes ending early is okay (if we are sub-parsing), but if we're at the root // level and there are still tokens left, we should return an UnparseableNode if (this.peek() !== null && minBindingPower === 0) { return UnparseableNode.from( resultNode, this.strand.tokens.slice(this._position) ); } return left; } public parseSubStrand( strandOrPath: StrandPath | Strand // activeFlags: Set = this.activeFlags ): CaretNode { const path = strandOrPath instanceof Strand ? this.editorState.content.findStrandPath(strandOrPath) : strandOrPath; if (!path) { throw new Error( `Strand not found in editorState: ${JSON.stringify(strandOrPath)}` ); } let tokenAtPath: Token | null = null; if (path.length > 0) { tokenAtPath = this.editorState.content.getToken({ strandPath: path.slice(0, -1), tokenIndex: path[path.length - 1].tokenIndex, }); } const subParser = new CaretParser( this._editorState, path, this._parselets // activeFlags ); return subParser.parse(); } public peek(): Token | null { if (this._position < this.strand.tokens.length) { return this.strand.tokens[this._position]; } return null; } public consume(): Token | null { if (this._position < this.strand.tokens.length) { return this.strand.tokens[this._position++]; } return null; } public done(): boolean { return this._position >= this.strand.tokens.length; } private filterParselets< T extends "leaf" | "prefix" | "infix" | "postfix" | "juxtaposition" >(validTypes: T[]): ParseletType[] { return this._parselets.filter((parselet) => validTypes.some((typeName) => { switch (typeName) { case "leaf": return parselet instanceof CaretLeafParselet; case "prefix": return parselet instanceof CaretPrefixParselet; case "infix": return parselet instanceof CaretInfixParselet; case "postfix": return parselet instanceof CaretPostfixParselet; case "juxtaposition": return parselet instanceof CaretJuxtapositionParselet; default: typeName satisfies never; return false; } }) ) as ParseletType[]; } private getBindingPower(parselet: CaretParselet): number { const bindingPower = this._parseletBindingPower.get(parselet); if (bindingPower === undefined) { throw new Error(`Parselet not registered: ${parselet.constructor.name}`); } return bindingPower; } } type ParseletType< T extends "leaf" | "prefix" | "infix" | "postfix" | "juxtaposition" > = | (T extends "leaf" ? CaretLeafParselet : never) | (T extends "prefix" ? CaretPrefixParselet : never) | (T extends "infix" ? CaretInfixParselet : never) | (T extends "postfix" ? CaretPostfixParselet : never) | (T extends "juxtaposition" ? CaretJuxtapositionParselet : never);