A world-class math input for the web
1import {
2 CaretJuxtapositionParselet,
3 CaretNode,
4 CaretParser,
5 IsErrorNodeTag,
6 NodeTag,
7} from "@caret-js/core";
8import { MultiplyNode } from "../nodes/multiply";
9import { MathExpressionTag } from "../tags/mathExpression";
10
11/**
12 * There are some relatively complicated rules for deciding when adjacent multiplication is/isn't allowed.
13 * The rules are implemented using tags so that custom node types can specify their own behavior.
14 *
15 * There are four tags that nodes can use to control whether or not they appear in adjacent multiplication:
16 * 1. `MathExpressionTag`: This tag indicates that a node represents a math expression (as opposed to, for example,
17 * a math statement like an equation, or a non-math node). Nodes with this tag are enrolled in the default adjacent
18 * multiplication behavior, which is to accept math expression nodes or error nodes on both sides only.
19 * 2. `IsErrorNodeTag`: This tag indicates that a node is an error node (like MissingNode), which is allowed to appear
20 * in adjacent multiplication by default so that as much of the expression as possible can be parsed and errors can be
21 * contained.
22 * 3. `CanBeAdjacentMultLeftSideTag`: This tag can be added to nodes to customize whether they can appear on the left side.
23 * The tag can either contain a boolean value (true/false) or a function that takes the left and right nodes
24 * and returns a boolean. (Both nodes must return true for adjacent multiplication to be allowed.)
25 * 4. `CanBeAdjacentMultRightSideTag`: This tag can be added to nodes to customize whether they can appear on the right side.
26 * The tag can either contain a boolean value (true/false) or a function that takes the left and right nodes
27 * and returns a boolean. (Both nodes must return true for adjacent multiplication to be allowed.)
28 *
29 * A node that does not have any of these tags will never appear in adjacent multiplication.
30 */
31
32export class AdjacentMultiplicationParselet extends CaretJuxtapositionParselet {
33 canParse(left: CaretNode, right: CaretNode): boolean {
34 return isLeftAllowed(left, right) && isRightAllowed(left, right);
35 }
36
37 parse(parser: CaretParser, left: CaretNode, right: CaretNode): CaretNode {
38 return MultiplyNode.from([left, right]).addTag(
39 new MultiplicationStyleTag("implicit")
40 );
41 }
42}
43
44function isLeftAllowed(left: CaretNode, right: CaretNode): boolean {
45 if (
46 left instanceof MultiplyNode &&
47 left.getTag(MultiplicationStyleTag)?.style === "implicit" &&
48 left.factors.length > 0
49 ) {
50 return isLeftAllowed(left.factors.at(-1)!, right);
51 }
52
53 const leftTag = left.getTag(CanBeAdjacentMultLeftSideTag);
54 if (leftTag) {
55 if (typeof leftTag.condition === "boolean") {
56 return leftTag.condition;
57 } else {
58 return leftTag.condition(left, right);
59 }
60 }
61
62 return left.hasTag(MathExpressionTag) || left.hasTag(IsErrorNodeTag);
63}
64
65function isRightAllowed(left: CaretNode, right: CaretNode): boolean {
66 if (
67 right instanceof MultiplyNode &&
68 right.getTag(MultiplicationStyleTag)?.style === "implicit" &&
69 right.factors.length > 0
70 ) {
71 return isRightAllowed(left, right.factors.at(0)!);
72 }
73
74 const rightTag = right.getTag(CanBeAdjacentMultRightSideTag);
75 if (rightTag) {
76 if (typeof rightTag.condition === "boolean") {
77 return rightTag.condition;
78 } else {
79 return rightTag.condition(left, right);
80 }
81 }
82
83 return right.hasTag(MathExpressionTag) || right.hasTag(IsErrorNodeTag);
84}
85
86export class CanBeAdjacentMultLeftSideTag<N extends CaretNode> extends NodeTag<
87 false,
88 N
89> {
90 public readonly allowMultiple = false;
91
92 constructor(
93 public readonly condition:
94 | boolean
95 | ((left: N, right: CaretNode) => boolean)
96 ) {
97 super();
98 }
99
100 equals(other: this): boolean {
101 return (
102 other instanceof CanBeAdjacentMultLeftSideTag &&
103 other.condition === this.condition
104 );
105 }
106}
107
108export class CanBeAdjacentMultRightSideTag<N extends CaretNode> extends NodeTag<
109 false,
110 N
111> {
112 public readonly allowMultiple = false;
113
114 constructor(
115 public readonly condition:
116 | boolean
117 | ((left: CaretNode, right: N) => boolean)
118 ) {
119 super();
120 }
121
122 equals(other: this): boolean {
123 return (
124 other instanceof CanBeAdjacentMultRightSideTag &&
125 other.condition === this.condition
126 );
127 }
128}
129
130export class MultiplicationStyleTag extends NodeTag<false> {
131 public readonly allowMultiple = false;
132
133 constructor(public readonly style: "implicit" | "dot" | "cross") {
134 super();
135 }
136
137 equals(other: this): boolean {
138 return (
139 other instanceof MultiplicationStyleTag && other.style === this.style
140 );
141 }
142}