A world-class math input for the web
1import { indent } from "./utils/indent";
2import { h, VNode } from "./vdom";
3
4export abstract class CaretNode {
5 private _tags: NodeTag<boolean>[] = [];
6
7 addTag(tag: NodeTag<boolean, this>): this {
8 if (!tag.allowMultiple) {
9 // Remove any existing tags of the same type
10 let existingTag = this._tags.find((t) => t instanceof tag.constructor);
11 if (existingTag) {
12 if (existingTag.combine) {
13 existingTag.combine(tag);
14 } else {
15 this._tags = this._tags.filter((t) => t !== existingTag);
16 this._tags.push(tag);
17 }
18 return this;
19 }
20 }
21
22 this._tags.push(tag);
23 return this;
24 }
25
26 getTag<T extends NodeTag<false, this>>(
27 type: new (...args: any[]) => T
28 ): T | undefined {
29 return this._tags.find((tag) => tag instanceof type) as T | undefined;
30 }
31
32 getTags<T extends NodeTag<true, this>>(type: new (...args: any[]) => T): T[] {
33 return this._tags.filter((tag) => tag instanceof type) as T[];
34 }
35
36 hasTag(
37 tagMatcher: (new (...args: any[]) => NodeTag<boolean>) | NodeTag<boolean>
38 ): boolean {
39 return [...this._tags].some((t) => {
40 if (tagMatcher instanceof NodeTag) {
41 return t.equals(tagMatcher);
42 } else {
43 return t instanceof tagMatcher;
44 }
45 });
46 }
47
48 mergeTagsFromNodes(nodes: CaretNode[]): this {
49 for (const node of nodes) {
50 for (const tag of node._tags) {
51 this.addTag(tag as NodeTag<boolean, this>);
52 }
53 }
54 return this;
55 }
56
57 /** @deprecated This property is only for debugging purposes. */
58 toDebugString(): string {
59 // Loop through public properties and print their names and values
60 // Example output: AddNode(addends=[DecimalNode(value="2.5"), DecimalNode(value="3.5")])
61 const className = this.constructor.name;
62 const props = Object.entries(this)
63 .filter(([key, _]) => !key.startsWith("_"))
64 .map(([key, value]) => {
65 if (Array.isArray(value)) {
66 return `${key}=[\n${indent(
67 value
68 .map((v) =>
69 v instanceof CaretNode ? v.toDebugString() : v.toString()
70 )
71 .join("\n")
72 )}\n]`;
73 } else if (value instanceof CaretNode) {
74 return `${key}=${value.toDebugString()}`;
75 } else {
76 return `${key}=${JSON.stringify(value)}`;
77 }
78 })
79 .filter(Boolean)
80 .join("\n");
81 return `<strong>${className}</strong>\n${indent(`${props}`)}`;
82 }
83
84 /** @deprecated This property is only for debugging purposes. */
85 toDebugHTML(): string {
86 const name = this.constructor.name;
87 return `<div style="display:inline-block; border:1px solid black; padding:4px;">
88 <strong>${name}</strong>
89 <div style="display:flex; gap: 2px;">
90 ${Object.entries(this)
91 .filter(([key, _]) => !key.startsWith("_"))
92 .map(([key, value]) => {
93 if (key === "start" || key === "end") {
94 return "";
95 }
96 return toDebugHTML(key + ":", value);
97 })
98 .join("\n")}
99 </div>
100 </div>`;
101 }
102
103 /** @deprecated This property is only for debugging purposes. */
104 toDebugMathML(): VNode {
105 return h("mrow", {});
106 }
107
108 *traverse(): Generator<CaretNode> {
109 yield this;
110
111 for (const [key, value] of Object.entries(this)) {
112 if (key.startsWith("_")) continue;
113
114 if (Array.isArray(value)) {
115 for (const v of value) {
116 if (v instanceof CaretNode) {
117 yield* v.traverse();
118 }
119 }
120 } else if (value instanceof CaretNode) {
121 yield* value.traverse();
122 }
123 }
124 }
125}
126
127function toDebugHTML(name: string, value: any): string {
128 if (Array.isArray(value)) {
129 return `<div style="border:1px solid gray; padding: 2px;">${name}<div style="display: flex; gap: 2px">${value
130 .map((v, i) => toDebugHTML(`${i}:`, v))
131 .join("")}</div></div>`;
132 } else if (value instanceof CaretNode) {
133 return `<div style="border:1px solid gray; padding: 2px;">${name}${value.toDebugHTML()}</div>`;
134 } else {
135 return `<div style="border:1px solid gray; padding: 2px;">${name}<div>${JSON.stringify(
136 value
137 )}</div></div>`;
138 }
139}
140
141export abstract class NodeTag<
142 AllowMultiple extends boolean,
143 _NodeType extends CaretNode = CaretNode
144> {
145 public abstract readonly allowMultiple: AllowMultiple;
146 abstract equals(other: this): boolean;
147 combine?(other: this): void;
148}
149
150export class IsErrorNodeTag extends NodeTag<false> {
151 public readonly allowMultiple = false;
152 equals(other: this): boolean {
153 return other instanceof IsErrorNodeTag;
154 }
155}