A world-class math input for the web
at main 142 lines 4.7 kB view raw
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}