A world-class math input for the web
1import { CaretNode } from "./node";
2import { CaretParser } from "./parser";
3
4export type CaretParselet =
5 | CaretLeafParselet
6 | CaretPrefixParselet
7 | CaretInfixParselet
8 | CaretPostfixParselet
9 | CaretJuxtapositionParselet;
10
11abstract class CaretParseletBase {
12 assert(
13 condition: boolean,
14 errorMessage = `Assertion failed in ${this.constructor.name} parselet`
15 ): asserts condition {
16 if (!condition) {
17 throw new CannotParseError(
18 this as unknown as CaretParselet,
19 errorMessage
20 );
21 }
22 }
23}
24
25/**
26 * A parselet used to handle leaf nodes. For example, variables and numbers ("x" or "42") can be implemented as leaf parselets.
27 *
28 * This parselet type is also used to handle standalone tokens like fractions and parentheses that contain their own sub-expressions
29 * but do not expect to be paired with any particular sibling tokens.
30 */
31export abstract class CaretLeafParselet extends CaretParseletBase {
32 /**
33 * Parse beginning from here and return the resulting CaretNode.
34 * @param parser The parser instance.
35 * @returns The resulting `CaretNode`.
36 * @throws `CannotParseError` if this parselet cannot parse at the current position.
37 */
38 abstract parse(parser: CaretParser): CaretNode;
39}
40
41/**
42 * A parselet used to handle prefix operators. For example, negation ("-a") can be implemented as a prefix parselet.
43 */
44export abstract class CaretPrefixParselet extends CaretParseletBase {
45 /**
46 * Parse beginning from here and return the resulting CaretNode.
47 * @param parser The parser instance.
48 * @returns The resulting `CaretNode`.
49 * @throws `CannotParseError` if this parselet cannot parse at the current position.
50 */
51 abstract parse(parser: CaretParser): CaretNode;
52}
53
54/**
55 * A parselet used to handle infix operators. For example, addition ("a + b") can be implemented as an infix parselet.
56 */
57export abstract class CaretInfixParselet extends CaretParseletBase {
58 /**
59 * The associativity of this infix operator: either "left" or "right". For example,
60 * addition is left-associative (a + b + c is parsed as (a + b) + c), while exponentiation
61 * is right-associative (a ^ b ^ c is parsed as a ^ (b ^ c)).
62 */
63 public abstract readonly associativity: "left" | "right";
64
65 /**
66 * Parse an infix expression starting with the given left-hand side node.
67 * @param parser The parser instance.
68 * @param left The left-hand side, which has already been parsed into a `CaretNode`.
69 * @returns The resulting `CaretNode`.
70 * @throws `CannotParseError` if this parselet cannot parse at the current position.
71 */
72 abstract parse(parser: CaretParser, left: CaretNode): CaretNode;
73}
74
75/**
76 * A parselet used to handle postfix operators. For example, factorial ("n!")
77 * can be implemented as a postfix parselet.
78 */
79export abstract class CaretPostfixParselet extends CaretParseletBase {
80 /**
81 * Parse a postfix expression starting with the given left-hand side node.
82 * @param parser The parser instance.
83 * @param left The left-hand side, which has already been parsed into a `CaretNode`.
84 * @returns The resulting `CaretNode`.
85 * @throws `CannotParseError` if this parselet cannot parse at the current position.
86 */
87 abstract parse(parser: CaretParser, left: CaretNode): CaretNode;
88}
89
90/**
91 * A parselet that handles adjacent nodes. For example, implicit multiplication ("2x" or "3(x+1)")
92 * can be implemented as a juxtaposition parselet.
93 */
94export abstract class CaretJuxtapositionParselet extends CaretParseletBase {
95 /**
96 * Determine whether this parselet can combine two adjacent ("juxtaposed") nodes. If true, the `parse()` method will be called.
97 * @param left The left-hand side node.
98 * @param right The right-hand side node.
99 * @returns `true` if this parselet can process these two nodes, `false` otherwise.
100 */
101 abstract canParse(left: CaretNode, right: CaretNode): boolean;
102
103 /**
104 * Parse two adjacent ("juxtaposed") nodes into a single CaretNode.
105 * @param parser The parser instance.
106 * @param left The left-hand side node.
107 * @param right The right-hand side node.
108 * @returns The resulting `CaretNode`.
109 */
110 abstract parse(
111 parser: CaretParser,
112 left: CaretNode,
113 right: CaretNode
114 ): CaretNode;
115}
116
117export class CannotParseError extends Error {
118 constructor(public parselet: CaretParselet, message?: string) {
119 super(message);
120 this.name = "CannotParseError";
121 }
122}