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