import { EditorState } from "./editorState"; import { StrandPath, strandPathsEqual, TokenPath } from "./path"; import type { Token } from "./token"; import { indent } from "./utils/indent"; import { h, t, VNode } from "./vdom"; export class Strand { public readonly tokens: readonly Token[]; private _parent: Token | EditorState | undefined; set parent(newParent: Token | EditorState) { if (this._parent !== undefined) { throw new Error("Parent is already set"); } this._parent = newParent; } get parent(): Token | EditorState | undefined { return this._parent; } get editorState(): EditorState | undefined { if (this._parent instanceof EditorState) { return this._parent; } return this._parent?.editorState; } constructor(tokens: readonly Token[]) { this.tokens = tokens; for (const token of this.tokens) { token.parentStrand = this; } } getStrand(path: StrandPath): Strand | null { if (path.length === 0) return this; const [level, ...restOfPath] = path; const token = this.tokens[level.tokenIndex]; if (!token) return null; const childStrand = token.children[level.childIndex]; if (!childStrand) return null; return childStrand.getStrand(restOfPath); } getToken(path: TokenPath): Token | null { const strand = this.getStrand(path.strandPath); if (!strand) return null; return strand.tokens[path.tokenIndex] ?? null; } findStrandPath(targetStrand: Strand): StrandPath | null { const path: StrandPath = []; let found = false; const searchStrand = (currentStrand: Strand, currentPath: StrandPath) => { if (currentStrand === targetStrand) { path.push(...currentPath); found = true; return; } for (let i = 0; i < currentStrand.tokens.length; i++) { const currentToken = currentStrand.tokens[i]; for (let j = 0; j < currentToken.children.length; j++) { const childStrand = currentToken.children[j]; searchStrand(childStrand, [ ...currentPath, { tokenIndex: i, childIndex: j }, ]); if (found) return; } } }; searchStrand(this, []); return found ? path : null; } findTokenPath(token: Token): TokenPath | null { const path: StrandPath = []; let found = false; const searchStrand = (currentStrand: Strand, currentPath: StrandPath) => { for (let i = 0; i < currentStrand.tokens.length; i++) { const currentToken = currentStrand.tokens[i]; if (currentToken === token) { path.push(...currentPath, { tokenIndex: i, childIndex: 0 }); found = true; return; } for (let j = 0; j < currentToken.children.length; j++) { const childStrand = currentToken.children[j]; searchStrand(childStrand, [ ...currentPath, { tokenIndex: i, childIndex: j }, ]); if (found) return; } } }; searchStrand(this, []); if (found) { const lastLevel = path.pop()!; return { strandPath: path, tokenIndex: lastLevel.tokenIndex }; } else { return null; } } splice(start: number, deleteCount: number, ...newTokens: Token[]): Strand { const beforeTokens = this.tokens.slice(0, start); const afterTokens = this.tokens.slice(start + deleteCount); return new Strand([...beforeTokens, ...newTokens, ...afterTokens]); } *traverseStrands( startingStrandPath: StrandPath = [] ): Generator<[Strand, StrandPath]> { yield [this, startingStrandPath]; for (let i = 0; i < this.tokens.length; i++) { const token = this.tokens[i]; for (let j = 0; j < token.children.length; j++) { const childStrand = token.children[j]; yield* childStrand.traverseStrands([ ...startingStrandPath, { tokenIndex: i, childIndex: j }, ]); } } } /** * @deprecated This function is only for debugging purposes. Actual rendering of tokens should be done via a separate rendering system. */ renderToDebugText(): string { const headPosition = this.editorState ? this.editorState.getPositionOfCursorInStrand( this.editorState.content.findStrandPath(this)!, this.editorState.selection.head ) : null; const anchorPosition = this.editorState ? this.editorState.getPositionOfCursorInStrand( this.editorState.content.findStrandPath(this)!, this.editorState.selection.anchor ) : null; const drawCursor = (index: number): string => { if (headPosition === index) { return "▮"; } if (anchorPosition === index) { return "▯"; } return ""; }; return `[\n${indent( this.tokens .map((token) => token.renderToDebugText()) .map((str, index) => `${drawCursor(index)}${str}`) .join(",\n") + drawCursor(this.tokens.length) )}\n]`; } /** * @deprecated This function is only for debugging purposes. Actual rendering of tokens should be done via a separate rendering system. */ renderToDebugHTML(): VNode { const { commonStrandPath, commonStartPos, commonEndPos } = this.editorState!.selection; const headPosition = this.editorState ? this.editorState.getPositionOfCursorInStrand( this.editorState.content.findStrandPath(this)!, this.editorState.selection.head ) : null; const cursorSpot = (index: number) => { if (headPosition === index) { return h("span", { class: "cursor" }, t("\u{200B}")); } return null; }; const thisPath = this.editorState?.content.findStrandPath(this); const isCommonStrand: boolean = !!thisPath && strandPathsEqual(thisPath, commonStrandPath); const isSelected = (index: number): boolean => { if (!isCommonStrand) return false; return index >= commonStartPos && index < commonEndPos; }; return h( "span", { class: "strand" }, ...this.tokens.flatMap((token, index) => { let renderedToken = token.renderToDebugHTML(); if (isSelected(index)) { renderedToken = h("span", { class: "selected" }, renderedToken); } return [cursorSpot(index), renderedToken]; }), cursorSpot(this.tokens.length) ); } /** * @deprecated This function is only for debugging purposes. Actual rendering of tokens should be done via a separate rendering system. */ renderToDebugMathML(): VNode { if (this.tokens.length === 1) { return this.tokens[0].renderToDebugMathML(); } return h( "mrow", {}, ...this.tokens.map((token) => token.renderToDebugMathML()) ); } }