import { indent } from "./utils/indent"; import { h, VNode } from "./vdom"; export abstract class CaretNode { private _tags: NodeTag[] = []; addTag(tag: NodeTag): this { if (!tag.allowMultiple) { // Remove any existing tags of the same type let existingTag = this._tags.find((t) => t instanceof tag.constructor); if (existingTag) { if (existingTag.combine) { existingTag.combine(tag); } else { this._tags = this._tags.filter((t) => t !== existingTag); this._tags.push(tag); } return this; } } this._tags.push(tag); return this; } getTag>( type: new (...args: any[]) => T ): T | undefined { return this._tags.find((tag) => tag instanceof type) as T | undefined; } getTags>(type: new (...args: any[]) => T): T[] { return this._tags.filter((tag) => tag instanceof type) as T[]; } hasTag( tagMatcher: (new (...args: any[]) => NodeTag) | NodeTag ): boolean { return [...this._tags].some((t) => { if (tagMatcher instanceof NodeTag) { return t.equals(tagMatcher); } else { return t instanceof tagMatcher; } }); } mergeTagsFromNodes(nodes: CaretNode[]): this { for (const node of nodes) { for (const tag of node._tags) { this.addTag(tag as NodeTag); } } return this; } /** @deprecated This property is only for debugging purposes. */ toDebugString(): string { // Loop through public properties and print their names and values // Example output: AddNode(addends=[DecimalNode(value="2.5"), DecimalNode(value="3.5")]) const className = this.constructor.name; const props = Object.entries(this) .filter(([key, _]) => !key.startsWith("_")) .map(([key, value]) => { if (Array.isArray(value)) { return `${key}=[\n${indent( value .map((v) => v instanceof CaretNode ? v.toDebugString() : v.toString() ) .join("\n") )}\n]`; } else if (value instanceof CaretNode) { return `${key}=${value.toDebugString()}`; } else { return `${key}=${JSON.stringify(value)}`; } }) .filter(Boolean) .join("\n"); return `${className}\n${indent(`${props}`)}`; } /** @deprecated This property is only for debugging purposes. */ toDebugHTML(): string { const name = this.constructor.name; return `
${name}
${Object.entries(this) .filter(([key, _]) => !key.startsWith("_")) .map(([key, value]) => { if (key === "start" || key === "end") { return ""; } return toDebugHTML(key + ":", value); }) .join("\n")}
`; } /** @deprecated This property is only for debugging purposes. */ toDebugMathML(): VNode { return h("mrow", {}); } *traverse(): Generator { yield this; for (const [key, value] of Object.entries(this)) { if (key.startsWith("_")) continue; if (Array.isArray(value)) { for (const v of value) { if (v instanceof CaretNode) { yield* v.traverse(); } } } else if (value instanceof CaretNode) { yield* value.traverse(); } } } } function toDebugHTML(name: string, value: any): string { if (Array.isArray(value)) { return `
${name}
${value .map((v, i) => toDebugHTML(`${i}:`, v)) .join("")}
`; } else if (value instanceof CaretNode) { return `
${name}${value.toDebugHTML()}
`; } else { return `
${name}
${JSON.stringify( value )}
`; } } export abstract class NodeTag< AllowMultiple extends boolean, _NodeType extends CaretNode = CaretNode > { public abstract readonly allowMultiple: AllowMultiple; abstract equals(other: this): boolean; combine?(other: this): void; } export class IsErrorNodeTag extends NodeTag { public readonly allowMultiple = false; equals(other: this): boolean { return other instanceof IsErrorNodeTag; } }