A world-class math input for the web
1import { Cursor } from "./cursor";
2import { StrandPath } from "./path";
3
4export class SelectionRange {
5 readonly anchor: Cursor;
6 readonly head: Cursor;
7
8 constructor(anchor = new Cursor(), head = anchor.clone()) {
9 this.anchor = anchor;
10 this.head = head;
11 }
12
13 get commonStrandPath(): StrandPath {
14 const minLength = Math.min(
15 this.anchor.strandPath.length,
16 this.head.strandPath.length
17 );
18 const commonPath: StrandPath = [];
19 for (let i = 0; i < minLength; i++) {
20 const anchorLevel = this.anchor.strandPath[i];
21 const headLevel = this.head.strandPath[i];
22 if (
23 anchorLevel.tokenIndex === headLevel.tokenIndex &&
24 anchorLevel.childIndex === headLevel.childIndex
25 ) {
26 commonPath.push(anchorLevel);
27 } else {
28 break;
29 }
30 }
31 return commonPath;
32 }
33
34 get commonStartPos(): number {
35 const anchorPos = this.projectCursorPosOnCommonStrand(this.anchor, "left");
36 const headPos = this.projectCursorPosOnCommonStrand(this.head, "left");
37 return Math.min(anchorPos, headPos);
38 }
39
40 get commonEndPos(): number {
41 const anchorPos = this.projectCursorPosOnCommonStrand(this.anchor, "right");
42 const headPos = this.projectCursorPosOnCommonStrand(this.head, "right");
43 return Math.max(anchorPos, headPos);
44 }
45
46 private projectCursorPosOnCommonStrand(
47 cursor: Cursor,
48 bias: "left" | "right"
49 ): number {
50 const commonStrandPath = this.commonStrandPath;
51 for (let i = 0; i < commonStrandPath.length; i++) {
52 const { tokenIndex, childIndex } = cursor.strandPath[i];
53 const commonLevel = commonStrandPath[i];
54 if (
55 tokenIndex !== commonLevel.tokenIndex ||
56 childIndex !== commonLevel.childIndex
57 ) {
58 throw new Error("Cursor is not on or within the common strand");
59 }
60 }
61
62 if (cursor.strandPath.length === commonStrandPath.length) {
63 return cursor.pos;
64 }
65
66 const { tokenIndex } = cursor.strandPath[commonStrandPath.length];
67 if (bias === "left") {
68 return tokenIndex;
69 } else {
70 return tokenIndex + 1;
71 }
72 }
73
74 isCollapsed(): boolean {
75 return this.anchor.equals(this.head);
76 }
77}