@khanacademy/math-input 0.4.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -1
  3. package/{build/math-input.css → dist/es/index.css} +0 -150
  4. package/dist/es/index.js +7798 -0
  5. package/dist/es/index.js.map +1 -0
  6. package/dist/index.css +586 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +7768 -0
  9. package/dist/index.js.flow +2 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/strings.js +71 -0
  12. package/index.html +20 -0
  13. package/less/echo.less +56 -0
  14. package/less/main.less +5 -0
  15. package/less/overrides.less +129 -0
  16. package/less/popover.less +22 -0
  17. package/less/tabbar.less +6 -0
  18. package/package.json +60 -89
  19. package/src/actions/index.js +57 -0
  20. package/src/components/__tests__/gesture-state-machine_test.js +437 -0
  21. package/src/components/__tests__/node-manager_test.js +89 -0
  22. package/src/components/__tests__/two-page-keypad_test.js +42 -0
  23. package/src/components/app.js +73 -0
  24. package/src/components/common-style.js +47 -0
  25. package/src/components/compute-layout-parameters.js +157 -0
  26. package/src/components/corner-decal.js +56 -0
  27. package/src/components/echo-manager.js +160 -0
  28. package/src/components/empty-keypad-button.js +49 -0
  29. package/src/components/expression-keypad.js +323 -0
  30. package/src/components/fraction-keypad.js +176 -0
  31. package/src/components/gesture-manager.js +226 -0
  32. package/src/components/gesture-state-machine.js +283 -0
  33. package/src/components/icon.js +74 -0
  34. package/src/components/iconography/arrow.js +22 -0
  35. package/src/components/iconography/backspace.js +29 -0
  36. package/src/components/iconography/cdot.js +29 -0
  37. package/src/components/iconography/cos.js +30 -0
  38. package/src/components/iconography/cube-root.js +36 -0
  39. package/src/components/iconography/dismiss.js +25 -0
  40. package/src/components/iconography/divide.js +34 -0
  41. package/src/components/iconography/down.js +16 -0
  42. package/src/components/iconography/equal.js +33 -0
  43. package/src/components/iconography/exp-2.js +29 -0
  44. package/src/components/iconography/exp-3.js +29 -0
  45. package/src/components/iconography/exp.js +29 -0
  46. package/src/components/iconography/frac.js +44 -0
  47. package/src/components/iconography/geq.js +33 -0
  48. package/src/components/iconography/gt.js +33 -0
  49. package/src/components/iconography/index.js +45 -0
  50. package/src/components/iconography/jump-into-numerator.js +41 -0
  51. package/src/components/iconography/jump-out-base.js +30 -0
  52. package/src/components/iconography/jump-out-denominator.js +41 -0
  53. package/src/components/iconography/jump-out-exponent.js +30 -0
  54. package/src/components/iconography/jump-out-numerator.js +41 -0
  55. package/src/components/iconography/jump-out-parentheses.js +33 -0
  56. package/src/components/iconography/left-paren.js +33 -0
  57. package/src/components/iconography/left.js +16 -0
  58. package/src/components/iconography/leq.js +33 -0
  59. package/src/components/iconography/ln.js +29 -0
  60. package/src/components/iconography/log-n.js +29 -0
  61. package/src/components/iconography/log.js +29 -0
  62. package/src/components/iconography/lt.js +33 -0
  63. package/src/components/iconography/minus.js +32 -0
  64. package/src/components/iconography/neq.js +33 -0
  65. package/src/components/iconography/parens.js +33 -0
  66. package/src/components/iconography/percent.js +49 -0
  67. package/src/components/iconography/period.js +26 -0
  68. package/src/components/iconography/plus.js +32 -0
  69. package/src/components/iconography/radical.js +36 -0
  70. package/src/components/iconography/right-paren.js +33 -0
  71. package/src/components/iconography/right.js +16 -0
  72. package/src/components/iconography/sin.js +30 -0
  73. package/src/components/iconography/sqrt.js +32 -0
  74. package/src/components/iconography/tan.js +30 -0
  75. package/src/components/iconography/times.js +33 -0
  76. package/src/components/iconography/up.js +16 -0
  77. package/src/components/input/__tests__/context-tracking_test.js +177 -0
  78. package/src/components/input/__tests__/math-wrapper.jsx +33 -0
  79. package/src/components/input/__tests__/mathquill_test.js +747 -0
  80. package/src/components/input/cursor-contexts.js +29 -0
  81. package/src/components/input/cursor-handle.js +137 -0
  82. package/src/components/input/drag-listener.js +75 -0
  83. package/src/components/input/math-input.js +924 -0
  84. package/src/components/input/math-wrapper.js +959 -0
  85. package/src/components/input/scroll-into-view.js +72 -0
  86. package/src/components/keypad/button-assets.js +492 -0
  87. package/src/components/keypad/button.js +106 -0
  88. package/src/components/keypad/button.stories.js +29 -0
  89. package/src/components/keypad/index.js +64 -0
  90. package/src/components/keypad/keypad-page-items.js +106 -0
  91. package/src/components/keypad/keypad-pages.stories.js +32 -0
  92. package/src/components/keypad/keypad.stories.js +35 -0
  93. package/src/components/keypad/numeric-input-page.js +100 -0
  94. package/src/components/keypad/pre-algebra-page.js +98 -0
  95. package/src/components/keypad/trigonometry-page.js +90 -0
  96. package/src/components/keypad-button.js +366 -0
  97. package/src/components/keypad-container.js +303 -0
  98. package/src/components/keypad.js +154 -0
  99. package/src/components/many-keypad-button.js +44 -0
  100. package/src/components/math-icon.js +65 -0
  101. package/src/components/multi-symbol-grid.js +182 -0
  102. package/src/components/multi-symbol-popover.js +59 -0
  103. package/src/components/navigation-pad.js +139 -0
  104. package/src/components/node-manager.js +129 -0
  105. package/src/components/popover-manager.js +76 -0
  106. package/src/components/popover-state-machine.js +173 -0
  107. package/src/components/prop-types.js +82 -0
  108. package/src/components/provided-keypad.js +103 -0
  109. package/src/components/styles.js +38 -0
  110. package/src/components/svg-icon.js +25 -0
  111. package/src/components/tabbar/__tests__/tabbar_test.js +65 -0
  112. package/src/components/tabbar/icons.js +69 -0
  113. package/src/components/tabbar/item.js +138 -0
  114. package/src/components/tabbar/tabbar.js +61 -0
  115. package/src/components/tabbar/tabbar.stories.js +60 -0
  116. package/src/components/tabbar/types.js +3 -0
  117. package/src/components/text-icon.js +52 -0
  118. package/src/components/touchable-keypad-button.js +146 -0
  119. package/src/components/two-page-keypad.js +99 -0
  120. package/src/components/velocity-tracker.js +76 -0
  121. package/src/components/z-indexes.js +9 -0
  122. package/src/consts.js +74 -0
  123. package/src/data/key-configs.js +349 -0
  124. package/src/data/keys.js +72 -0
  125. package/src/demo.js +8 -0
  126. package/src/fake-react-native-web/index.js +12 -0
  127. package/src/fake-react-native-web/text.js +56 -0
  128. package/src/fake-react-native-web/view.js +91 -0
  129. package/src/index.js +14 -0
  130. package/src/native-app.js +84 -0
  131. package/src/store/index.js +505 -0
  132. package/src/utils.js +18 -0
  133. package/tools/svg-to-react/convert.py +111 -0
  134. package/tools/svg-to-react/icons/math-keypad-icon-0.svg +32 -0
  135. package/tools/svg-to-react/icons/math-keypad-icon-1.svg +32 -0
  136. package/tools/svg-to-react/icons/math-keypad-icon-2.svg +32 -0
  137. package/tools/svg-to-react/icons/math-keypad-icon-3.svg +32 -0
  138. package/tools/svg-to-react/icons/math-keypad-icon-4.svg +32 -0
  139. package/tools/svg-to-react/icons/math-keypad-icon-5.svg +32 -0
  140. package/tools/svg-to-react/icons/math-keypad-icon-6.svg +32 -0
  141. package/tools/svg-to-react/icons/math-keypad-icon-7.svg +32 -0
  142. package/tools/svg-to-react/icons/math-keypad-icon-8.svg +32 -0
  143. package/tools/svg-to-react/icons/math-keypad-icon-9.svg +32 -0
  144. package/tools/svg-to-react/icons/math-keypad-icon-addition.svg +34 -0
  145. package/tools/svg-to-react/icons/math-keypad-icon-cos.svg +38 -0
  146. package/tools/svg-to-react/icons/math-keypad-icon-delete.svg +36 -0
  147. package/tools/svg-to-react/icons/math-keypad-icon-dismiss.svg +36 -0
  148. package/tools/svg-to-react/icons/math-keypad-icon-division.svg +36 -0
  149. package/tools/svg-to-react/icons/math-keypad-icon-equals-not.svg +50 -0
  150. package/tools/svg-to-react/icons/math-keypad-icon-equals.svg +48 -0
  151. package/tools/svg-to-react/icons/math-keypad-icon-exponent-2.svg +38 -0
  152. package/tools/svg-to-react/icons/math-keypad-icon-exponent-3.svg +38 -0
  153. package/tools/svg-to-react/icons/math-keypad-icon-exponent.svg +38 -0
  154. package/tools/svg-to-react/icons/math-keypad-icon-fraction.svg +42 -0
  155. package/tools/svg-to-react/icons/math-keypad-icon-greater-than.svg +46 -0
  156. package/tools/svg-to-react/icons/math-keypad-icon-jump-out-base.svg +44 -0
  157. package/tools/svg-to-react/icons/math-keypad-icon-jump-out-denominator.svg +48 -0
  158. package/tools/svg-to-react/icons/math-keypad-icon-jump-out-exponent.svg +44 -0
  159. package/tools/svg-to-react/icons/math-keypad-icon-jump-out-parentheses.svg +44 -0
  160. package/tools/svg-to-react/icons/math-keypad-icon-less-than.svg +46 -0
  161. package/tools/svg-to-react/icons/math-keypad-icon-log-10.svg +36 -0
  162. package/tools/svg-to-react/icons/math-keypad-icon-log-e.svg +36 -0
  163. package/tools/svg-to-react/icons/math-keypad-icon-log.svg +38 -0
  164. package/tools/svg-to-react/icons/math-keypad-icon-multiplication-cross.svg +40 -0
  165. package/tools/svg-to-react/icons/math-keypad-icon-multiplication-dot.svg +38 -0
  166. package/tools/svg-to-react/icons/math-keypad-icon-percent.svg +42 -0
  167. package/tools/svg-to-react/icons/math-keypad-icon-radical-2.svg +36 -0
  168. package/tools/svg-to-react/icons/math-keypad-icon-radical-3.svg +38 -0
  169. package/tools/svg-to-react/icons/math-keypad-icon-radical.svg +38 -0
  170. package/tools/svg-to-react/icons/math-keypad-icon-radix-character.svg +32 -0
  171. package/tools/svg-to-react/icons/math-keypad-icon-sin.svg +38 -0
  172. package/tools/svg-to-react/icons/math-keypad-icon-subtraction.svg +32 -0
  173. package/tools/svg-to-react/icons/math-keypad-icon-tan.svg +38 -0
  174. package/tools/svg-to-react/symbol_map.py +41 -0
  175. package/LICENSE.txt +0 -21
  176. package/build/math-input.js +0 -1
@@ -0,0 +1,959 @@
1
+ /**
2
+ * This file contains a wrapper around MathQuill so that we can provide a
3
+ * more regular interface for the functionality we need while insulating us
4
+ * from MathQuill changes.
5
+ */
6
+
7
+ import $ from "jquery";
8
+ import MathQuill from "mathquill";
9
+
10
+ import {DecimalSeparators} from "../../consts.js";
11
+ import Keys from "../../data/keys.js";
12
+ import {decimalSeparator} from "../../utils.js";
13
+
14
+ import * as CursorContexts from "./cursor-contexts.js";
15
+
16
+ // Keeping `window` in place for test suite and GitHub Pages.
17
+ // If it does not exist, fall back to CommonJS require. - jsatk
18
+
19
+ const decimalSymbol = decimalSeparator === DecimalSeparators.COMMA ? "," : ".";
20
+
21
+ const WRITE = "write";
22
+ const CMD = "cmd";
23
+ const KEYSTROKE = "keystroke";
24
+ const MQ_END = 0;
25
+
26
+ // A mapping from keys that can be pressed on a keypad to the way in which
27
+ // MathQuill should modify its input in response to that key-press. Any keys
28
+ // that do not provide explicit actions (like the numeral keys) will merely
29
+ // write their contents to MathQuill.
30
+ const KeyActions = {
31
+ [Keys.PLUS]: {str: "+", fn: WRITE},
32
+ [Keys.MINUS]: {str: "-", fn: WRITE},
33
+ [Keys.NEGATIVE]: {str: "-", fn: WRITE},
34
+ [Keys.TIMES]: {str: "\\times", fn: WRITE},
35
+ [Keys.DIVIDE]: {str: "\\div", fn: WRITE},
36
+ [Keys.DECIMAL]: {
37
+ str: decimalSymbol,
38
+ fn: WRITE,
39
+ },
40
+ [Keys.EQUAL]: {str: "=", fn: WRITE},
41
+ [Keys.NEQ]: {str: "\\neq", fn: WRITE},
42
+ [Keys.CDOT]: {str: "\\cdot", fn: WRITE},
43
+ [Keys.PERCENT]: {str: "%", fn: WRITE},
44
+ [Keys.LEFT_PAREN]: {str: "(", fn: CMD},
45
+ [Keys.RIGHT_PAREN]: {str: ")", fn: CMD},
46
+ [Keys.SQRT]: {str: "sqrt", fn: CMD},
47
+ [Keys.PI]: {str: "pi", fn: CMD},
48
+ [Keys.THETA]: {str: "theta", fn: CMD},
49
+ [Keys.RADICAL]: {str: "nthroot", fn: CMD},
50
+ [Keys.LT]: {str: "<", fn: WRITE},
51
+ [Keys.LEQ]: {str: "\\leq", fn: WRITE},
52
+ [Keys.GT]: {str: ">", fn: WRITE},
53
+ [Keys.GEQ]: {str: "\\geq", fn: WRITE},
54
+ [Keys.UP]: {str: "Up", fn: KEYSTROKE},
55
+ [Keys.DOWN]: {str: "Down", fn: KEYSTROKE},
56
+ // The `FRAC_EXCLUSIVE` variant is handled manually, since we may need to do
57
+ // some additional navigation depending on the cursor position.
58
+ [Keys.FRAC_INCLUSIVE]: {str: "/", fn: CMD},
59
+ };
60
+
61
+ const NormalCommands = {
62
+ [Keys.LOG]: "log",
63
+ [Keys.LN]: "ln",
64
+ [Keys.SIN]: "sin",
65
+ [Keys.COS]: "cos",
66
+ [Keys.TAN]: "tan",
67
+ };
68
+
69
+ const ArithmeticOperators = ["+", "-", "\\cdot", "\\times", "\\div"];
70
+ const EqualityOperators = ["=", "\\neq", "<", "\\leq", ">", "\\geq"];
71
+
72
+ const Numerals = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
73
+ const GreekLetters = ["\\theta", "\\pi"];
74
+ const Letters = [
75
+ "A",
76
+ "B",
77
+ "C",
78
+ "D",
79
+ "E",
80
+ "F",
81
+ "G",
82
+ "H",
83
+ "I",
84
+ "J",
85
+ "K",
86
+ "L",
87
+ "M",
88
+ "N",
89
+ "O",
90
+ "P",
91
+ "Q",
92
+ "R",
93
+ "S",
94
+ "T",
95
+ "U",
96
+ "V",
97
+ "W",
98
+ "X",
99
+ "Y",
100
+ "Z",
101
+ ];
102
+
103
+ // We only consider numerals, variables, and Greek Letters to be proper
104
+ // leaf nodes.
105
+ const ValidLeaves = [
106
+ ...Numerals,
107
+ ...GreekLetters,
108
+ ...Letters.map((letter) => letter.toLowerCase()),
109
+ ...Letters.map((letter) => letter.toUpperCase()),
110
+ ];
111
+
112
+ const KeysForJumpContext = {
113
+ [CursorContexts.IN_PARENS]: Keys.JUMP_OUT_PARENTHESES,
114
+ [CursorContexts.IN_SUPER_SCRIPT]: Keys.JUMP_OUT_EXPONENT,
115
+ [CursorContexts.IN_SUB_SCRIPT]: Keys.JUMP_OUT_BASE,
116
+ [CursorContexts.BEFORE_FRACTION]: Keys.JUMP_INTO_NUMERATOR,
117
+ [CursorContexts.IN_NUMERATOR]: Keys.JUMP_OUT_NUMERATOR,
118
+ [CursorContexts.IN_DENOMINATOR]: Keys.JUMP_OUT_DENOMINATOR,
119
+ };
120
+
121
+ class MathWrapper {
122
+ constructor(element, options = {}, callbacks = {}) {
123
+ this.MQ = MathQuill.getInterface(2);
124
+ this.mathField = this.MQ.MathField(element, {
125
+ // use a span instead of a textarea so that we don't bring up the
126
+ // native keyboard on mobile when selecting the input
127
+ substituteTextarea: function () {
128
+ return document.createElement("span");
129
+ },
130
+ });
131
+ this.callbacks = callbacks;
132
+ }
133
+
134
+ focus() {
135
+ // HACK(charlie): We shouldn't reaching into MathQuill internals like
136
+ // this, but it's the easiest way to allow us to manage the focus state
137
+ // ourselves.
138
+ const controller = this.mathField.__controller;
139
+ controller.cursor.show();
140
+
141
+ // Set MathQuill's internal state to reflect the focus, otherwise it
142
+ // will consistently try to hide the cursor on key-press and introduce
143
+ // layout jank.
144
+ controller.blurred = false;
145
+ }
146
+
147
+ blur() {
148
+ const controller = this.mathField.__controller;
149
+ controller.cursor.hide();
150
+ controller.blurred = true;
151
+ }
152
+
153
+ _writeNormalFunction(name) {
154
+ this.mathField.write(`\\${name}\\left(\\right)`);
155
+ this.mathField.keystroke("Left");
156
+ }
157
+
158
+ /**
159
+ * Handle a key press and return the resulting cursor state.
160
+ *
161
+ * @param {Key} key - an enum representing the key that was pressed
162
+ * @returns {object} a cursor object, consisting of a cursor context
163
+ */
164
+ pressKey(key) {
165
+ const cursor = this.mathField.__controller.cursor;
166
+
167
+ if (key in KeyActions) {
168
+ const {str, fn} = KeyActions[key];
169
+
170
+ if (str && fn) {
171
+ this.mathField[fn](str);
172
+ }
173
+ } else if (Object.keys(NormalCommands).includes(key)) {
174
+ this._writeNormalFunction(NormalCommands[key]);
175
+ } else if (key === Keys.FRAC_EXCLUSIVE) {
176
+ // If there's nothing to the left of the cursor, then we want to
177
+ // leave the cursor to the left of the fraction after creating it.
178
+ const shouldNavigateLeft = cursor[this.MQ.L] === MQ_END;
179
+ this.mathField.cmd("\\frac");
180
+ if (shouldNavigateLeft) {
181
+ this.mathField.keystroke("Left");
182
+ }
183
+ } else if (key === Keys.FRAC) {
184
+ // eslint-disable-next-line no-unused-vars
185
+ const shouldNavigateLeft = cursor[this.MQ.L] === MQ_END;
186
+ this.mathField.cmd("\\frac");
187
+ } else if (key === Keys.LOG_N) {
188
+ this.mathField.write("log_{ }\\left(\\right)");
189
+ this.mathField.keystroke("Left"); // into parentheses
190
+ this.mathField.keystroke("Left"); // out of parentheses
191
+ this.mathField.keystroke("Left"); // into index
192
+ } else if (key === Keys.CUBE_ROOT) {
193
+ this.mathField.write("\\sqrt[3]{}");
194
+ this.mathField.keystroke("Left"); // under the root
195
+ } else if (
196
+ key === Keys.EXP ||
197
+ key === Keys.EXP_2 ||
198
+ key === Keys.EXP_3
199
+ ) {
200
+ this._handleExponent(cursor, key);
201
+ } else if (
202
+ key === Keys.JUMP_OUT_PARENTHESES ||
203
+ key === Keys.JUMP_OUT_EXPONENT ||
204
+ key === Keys.JUMP_OUT_BASE ||
205
+ key === Keys.JUMP_INTO_NUMERATOR ||
206
+ key === Keys.JUMP_OUT_NUMERATOR ||
207
+ key === Keys.JUMP_OUT_DENOMINATOR
208
+ ) {
209
+ this._handleJumpOut(cursor, key);
210
+ } else if (key === Keys.BACKSPACE) {
211
+ this._handleBackspace(cursor);
212
+ } else if (key === Keys.LEFT) {
213
+ this._handleLeftArrow(cursor);
214
+ } else if (key === Keys.RIGHT || key === Keys.JUMP_OUT) {
215
+ this._handleRightArrow(cursor);
216
+ } else if (/^[a-zA-Z]$/.test(key)) {
217
+ this.mathField[WRITE](key);
218
+ } else if (/^NUM_\d/.test(key)) {
219
+ this.mathField[WRITE](key[4]);
220
+ }
221
+
222
+ if (!cursor.selection) {
223
+ // don't show the cursor for selections
224
+ cursor.show();
225
+ }
226
+
227
+ if (this.callbacks.onSelectionChanged) {
228
+ this.callbacks.onSelectionChanged(cursor.selection);
229
+ }
230
+
231
+ // NOTE(charlie): It's insufficient to do this as an `edited` handler
232
+ // on the MathField, as that handler isn't triggered on navigation
233
+ // events.
234
+ return {
235
+ context: this.contextForCursor(cursor),
236
+ };
237
+ }
238
+
239
+ /**
240
+ * Place the cursor beside the node located at the given coordinates.
241
+ *
242
+ * @param {number} x - the x coordinate in the viewport
243
+ * @param {number} y - the y coordinate in the viewport
244
+ * @param {Node} hitNode - the node next to which the cursor should be
245
+ * placed; if provided, the coordinates will be used
246
+ * to determine on which side of the node the cursor
247
+ * should be placed
248
+ */
249
+ setCursorPosition(x, y, hitNode) {
250
+ const el = hitNode || document.elementFromPoint(x, y);
251
+
252
+ if (el) {
253
+ const cursor = this.getCursor();
254
+
255
+ if (el.hasAttribute("mq-root-block")) {
256
+ // If we're in the empty area place the cursor at the right
257
+ // end of the expression.
258
+ cursor.insAtRightEnd(this.mathField.__controller.root);
259
+ } else {
260
+ // Otherwise place beside the element at x, y.
261
+ const controller = this.mathField.__controller;
262
+
263
+ const pageX = x - document.body.scrollLeft;
264
+ const pageY = y - document.body.scrollTop;
265
+ controller.seek($(el), pageX, pageY).cursor.startSelection();
266
+
267
+ // Unless that would leave us mid-command, in which case, we
268
+ // need to adjust and place the cursor inside the parens
269
+ // following the command.
270
+ const command = this._maybeFindCommand(cursor[this.MQ.L]);
271
+ if (command && command.endNode) {
272
+ // NOTE(charlie): endNode should definitely be \left(.
273
+ cursor.insLeftOf(command.endNode);
274
+ this.mathField.keystroke("Right");
275
+ }
276
+ }
277
+
278
+ if (this.callbacks.onCursorMove) {
279
+ this.callbacks.onCursorMove({
280
+ context: this.contextForCursor(cursor),
281
+ });
282
+ }
283
+ }
284
+ }
285
+
286
+ getCursor() {
287
+ return this.mathField.__controller.cursor;
288
+ }
289
+
290
+ getSelection() {
291
+ return this.getCursor().selection;
292
+ }
293
+
294
+ getContent() {
295
+ return this.mathField.latex();
296
+ }
297
+
298
+ setContent(latex) {
299
+ this.mathField.latex(latex);
300
+ }
301
+
302
+ isEmpty() {
303
+ const cursor = this.getCursor();
304
+ return cursor.parent.id === 1 && cursor[1] === 0 && cursor[-1] === 0;
305
+ }
306
+
307
+ // Notes about MathQuill
308
+ //
309
+ // MathQuill's stores its layout as nested linked lists. Each node in the
310
+ // list has this.MQ.L '-1' and this.MQ.R '1' properties that define links to
311
+ // the left and right nodes respectively. They also have
312
+ //
313
+ // ctrlSeq: contains the latex code snippet that defines that node.
314
+ // jQ: jQuery object for the DOM node(s) for this MathQuill node.
315
+ // ends: pointers to the nodes at the ends of the container.
316
+ // parent: parent node.
317
+ // blocks: an array containing one or more nodes that make up the node.
318
+ // sub?: subscript node if there is one as is the case in log_n
319
+ //
320
+ // All of the code below is super fragile. Please be especially careful
321
+ // when upgrading MathQuill.
322
+
323
+ _handleBackspaceInNthRoot(cursor) {
324
+ const isAtLeftEnd = cursor[this.MQ.L] === MQ_END;
325
+
326
+ const isRootEmpty = this._isInsideEmptyNode(
327
+ cursor.parent.parent.blocks[0].ends,
328
+ );
329
+
330
+ if (isAtLeftEnd) {
331
+ this._selectNode(cursor.parent.parent, cursor);
332
+
333
+ if (isRootEmpty) {
334
+ this.mathField.keystroke("Backspace");
335
+ }
336
+ } else {
337
+ this.mathField.keystroke("Backspace");
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Advances the cursor to the next logical position.
343
+ *
344
+ * @param {cursor} cursor
345
+ * @private
346
+ */
347
+ _handleJumpOut(cursor, key) {
348
+ const context = this.contextForCursor(cursor);
349
+
350
+ // Validate that the current cursor context matches the key's intent.
351
+ if (KeysForJumpContext[context] !== key) {
352
+ // If we don't have a valid cursor context, yet the user was able
353
+ // to trigger a jump-out key, that's a broken invariant. Rather
354
+ // than throw an error (which would kick the user out of the
355
+ // exercise), we do nothing, as a fallback strategy. The user can
356
+ // still move the cursor manually.
357
+ return;
358
+ }
359
+
360
+ switch (context) {
361
+ case CursorContexts.IN_PARENS:
362
+ // Insert at the end of the parentheses, and then navigate right
363
+ // once more to get 'beyond' the parentheses.
364
+ cursor.insRightOf(cursor.parent.parent);
365
+ break;
366
+
367
+ case CursorContexts.BEFORE_FRACTION:
368
+ // Find the nearest fraction to the right of the cursor.
369
+ let fractionNode;
370
+ let visitor = cursor;
371
+ while (visitor[this.MQ.R] !== MQ_END) {
372
+ if (this._isFraction(visitor[this.MQ.R])) {
373
+ fractionNode = visitor[this.MQ.R];
374
+ }
375
+ visitor = visitor[this.MQ.R];
376
+ }
377
+
378
+ // Jump into it!
379
+ cursor.insLeftOf(fractionNode);
380
+ this.mathField.keystroke("Right");
381
+ break;
382
+
383
+ case CursorContexts.IN_NUMERATOR:
384
+ // HACK(charlie): I can't find a better way to do this. The goal
385
+ // is to place the cursor at the start of the matching
386
+ // denominator. So, we identify the appropriate node, and
387
+ // continue rightwards until we find ourselves inside of it.
388
+ // It's possible that there are cases in which we don't reach
389
+ // the denominator, though I can't think of any.
390
+ const siblingDenominator = cursor.parent.parent.blocks[1];
391
+ while (cursor.parent !== siblingDenominator) {
392
+ this.mathField.keystroke("Right");
393
+ }
394
+ break;
395
+
396
+ case CursorContexts.IN_DENOMINATOR:
397
+ cursor.insRightOf(cursor.parent.parent);
398
+ break;
399
+
400
+ case CursorContexts.IN_SUB_SCRIPT:
401
+ // Insert just beyond the superscript.
402
+ cursor.insRightOf(cursor.parent.parent);
403
+
404
+ // Navigate right once more, if we're right before parens. This
405
+ // is to handle the standard case in which the subscript is the
406
+ // base of a custom log.
407
+ if (this._isParens(cursor[this.MQ.R])) {
408
+ this.mathField.keystroke("Right");
409
+ }
410
+ break;
411
+
412
+ case CursorContexts.IN_SUPER_SCRIPT:
413
+ // Insert just beyond the superscript.
414
+ cursor.insRightOf(cursor.parent.parent);
415
+ break;
416
+
417
+ default:
418
+ throw new Error(
419
+ `Attempted to 'Jump Out' from node, but found no ` +
420
+ `appropriate cursor context: ${context}`,
421
+ );
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Selects and deletes part of the expression based on the cursor location.
427
+ * See inline comments for precise behavior of different cases.
428
+ *
429
+ * @param {cursor} cursor
430
+ * @private
431
+ */
432
+ _handleBackspace(cursor) {
433
+ if (!cursor.selection) {
434
+ const parent = cursor.parent;
435
+ const grandparent = parent.parent;
436
+ const leftNode = cursor[this.MQ.L];
437
+
438
+ if (this._isFraction(leftNode)) {
439
+ this._selectNode(leftNode, cursor);
440
+ } else if (this._isSquareRoot(leftNode)) {
441
+ this._selectNode(leftNode, cursor);
442
+ } else if (this._isNthRoot(leftNode)) {
443
+ this._selectNode(leftNode, cursor);
444
+ } else if (this._isNthRootIndex(parent)) {
445
+ this._handleBackspaceInRootIndex(cursor);
446
+ } else if (leftNode.ctrlSeq === "\\left(") {
447
+ this._handleBackspaceOutsideParens(cursor);
448
+ } else if (grandparent.ctrlSeq === "\\left(") {
449
+ this._handleBackspaceInsideParens(cursor);
450
+ } else if (this._isInsideLogIndex(cursor)) {
451
+ this._handleBackspaceInLogIndex(cursor);
452
+ } else if (
453
+ leftNode.ctrlSeq === "\\ge " ||
454
+ leftNode.ctrlSeq === "\\le "
455
+ ) {
456
+ this._handleBackspaceAfterLigaturedSymbol(cursor);
457
+ } else if (this._isNthRoot(grandparent) && leftNode === MQ_END) {
458
+ this._handleBackspaceInNthRoot(cursor);
459
+ } else {
460
+ this.mathField.keystroke("Backspace");
461
+ }
462
+ } else {
463
+ this.mathField.keystroke("Backspace");
464
+ }
465
+ }
466
+
467
+ _handleLeftArrow(cursor) {
468
+ // If we're inside a function, and just after the left parentheses, we
469
+ // need to skip the entire function name, rather than move the cursor
470
+ // inside of it. For example, when hitting left from within the
471
+ // parentheses in `cos()`, we want to place the cursor to the left of
472
+ // the entire expression, rather than between the `s` and the left
473
+ // parenthesis.
474
+ // From the cursor's perspective, this requires that our left node is
475
+ // the MQ_END node, that our grandparent is the left parenthesis, and
476
+ // the nodes to the left of our grandparent comprise a valid function
477
+ // name.
478
+ if (cursor[this.MQ.L] === MQ_END) {
479
+ const parent = cursor.parent;
480
+ const grandparent = parent.parent;
481
+ if (grandparent.ctrlSeq === "\\left(") {
482
+ const command = this._maybeFindCommandBeforeParens(grandparent);
483
+ if (command) {
484
+ cursor.insLeftOf(command.startNode);
485
+ return;
486
+ }
487
+ }
488
+ }
489
+
490
+ // Otherwise, we default to the standard MathQull left behavior.
491
+ this.mathField.keystroke("Left");
492
+ }
493
+
494
+ _handleRightArrow(cursor) {
495
+ const command = this._maybeFindCommand(cursor[this.MQ.R]);
496
+ if (command) {
497
+ // Similarly, if a function is to our right, then we need to place
498
+ // the cursor at the start of its parenthetical content, which is
499
+ // done by putting it to the left of ites parentheses and then
500
+ // moving right once.
501
+ cursor.insLeftOf(command.endNode);
502
+ this.mathField.keystroke("Right");
503
+ } else {
504
+ // Otherwise, we default to the standard MathQull right behavior.
505
+ this.mathField.keystroke("Right");
506
+ }
507
+ }
508
+
509
+ _handleExponent(cursor, key) {
510
+ // If there's an invalid operator preceding the cursor (anything that
511
+ // knowingly cannot be raised to a power), add an empty set of
512
+ // parentheses and apply the exponent to that.
513
+ const invalidPrefixes = [...ArithmeticOperators, ...EqualityOperators];
514
+
515
+ const precedingNode = cursor[this.MQ.L];
516
+ const shouldPrefixWithParens =
517
+ precedingNode === MQ_END ||
518
+ invalidPrefixes.includes(precedingNode.ctrlSeq.trim());
519
+ if (shouldPrefixWithParens) {
520
+ this.mathField.write("\\left(\\right)");
521
+ }
522
+
523
+ // Insert the appropriate exponent operator.
524
+ switch (key) {
525
+ case Keys.EXP:
526
+ this.mathField.cmd("^");
527
+ break;
528
+
529
+ case Keys.EXP_2:
530
+ case Keys.EXP_3:
531
+ this.mathField.write(`^${key === Keys.EXP_2 ? 2 : 3}`);
532
+
533
+ // If we enter a square or a cube, we should leave the cursor
534
+ // within the newly inserted parens, if they exist. This takes
535
+ // exactly four left strokes, since the cursor by default would
536
+ // end up to the right of the exponent.
537
+ if (shouldPrefixWithParens) {
538
+ this.mathField.keystroke("Left");
539
+ this.mathField.keystroke("Left");
540
+ this.mathField.keystroke("Left");
541
+ this.mathField.keystroke("Left");
542
+ }
543
+ break;
544
+
545
+ default:
546
+ throw new Error(`Invalid exponent key: ${key}`);
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Return the start node, end node, and full name of the command of which
552
+ * the initial node is a part, or `null` if the node is not part of a
553
+ * command.
554
+ *
555
+ * @param {node} initialNode - the node to included as part of the command
556
+ * @returns {null|object} - `null` or an object containing the start node
557
+ * (`startNode`), end node (`endNode`), and full
558
+ * name (`name`) of the command
559
+ * @private
560
+ */
561
+ _maybeFindCommand(initialNode) {
562
+ if (!initialNode) {
563
+ return null;
564
+ }
565
+
566
+ // MathQuill stores commands as separate characters so that
567
+ // users can delete commands one character at a time. We iterate over
568
+ // the nodes from right to left until we hit a sequence starting with a
569
+ // '\\', which signifies the start of a command; then we iterate from
570
+ // left to right until we hit a '\\left(', which signifies the end of a
571
+ // command. If we encounter any character that doesn't belong in a
572
+ // command, we return null. We match a single character at a time.
573
+ // Ex) ['\\l', 'o', 'g ', '\\left(', ...]
574
+ const commandCharRegex = /^[a-z]$/;
575
+ const commandStartRegex = /^\\[a-z]$/;
576
+ const commandEndSeq = "\\left(";
577
+
578
+ // Note: We whitelist the set of valid commands, since relying solely on
579
+ // a command being prefixed with a backslash leads to undesired
580
+ // behavior. For example, Greek symbols, left parentheses, and square
581
+ // roots all get treated as commands.
582
+ const validCommands = ["\\log", "\\ln", "\\cos", "\\sin", "\\tan"];
583
+
584
+ let name = "";
585
+ let startNode;
586
+ let endNode;
587
+
588
+ // Collect the portion of the command from the current node, leftwards
589
+ // until the start of the command.
590
+ let node = initialNode;
591
+ while (node !== 0) {
592
+ const ctrlSeq = node.ctrlSeq.trim();
593
+ if (commandCharRegex.test(ctrlSeq)) {
594
+ name = ctrlSeq + name;
595
+ } else if (commandStartRegex.test(ctrlSeq)) {
596
+ name = ctrlSeq + name;
597
+ startNode = node;
598
+ break;
599
+ } else {
600
+ break;
601
+ }
602
+
603
+ node = node[this.MQ.L];
604
+ }
605
+
606
+ // If we hit the start of a command, then grab the rest of it by
607
+ // iterating rightwards to compute the full name of the command, along
608
+ // with its terminal node.
609
+ if (startNode) {
610
+ // Next, iterate from the start to the right.
611
+ node = initialNode[this.MQ.R];
612
+ while (node !== 0) {
613
+ const ctrlSeq = node.ctrlSeq.trim();
614
+ if (commandCharRegex.test(ctrlSeq)) {
615
+ // If we have a single character, add it to the command
616
+ // name.
617
+ name = name + ctrlSeq;
618
+ } else if (ctrlSeq === commandEndSeq) {
619
+ // If we hit the command end delimiter (the left
620
+ // parentheses surrounding its arguments), stop.
621
+ endNode = node;
622
+ break;
623
+ }
624
+
625
+ node = node[this.MQ.R];
626
+ }
627
+ if (validCommands.includes(name)) {
628
+ return {name, startNode, endNode};
629
+ } else {
630
+ return null;
631
+ }
632
+ } else {
633
+ return null;
634
+ }
635
+ }
636
+
637
+ /**
638
+ * Return the start node, end node, and full name of the command to the left
639
+ * of `\\left(`, or `null` if there is no command.
640
+ *
641
+ * @param {node} leftParenNode - node where .ctrlSeq == `\\left(`
642
+ * @returns {null|object} - `null` or an object containing the start node
643
+ * (`startNode`), end node (`endNode`), and full
644
+ * name (`name`) of the command
645
+ * @private
646
+ */
647
+ _maybeFindCommandBeforeParens(leftParenNode) {
648
+ return this._maybeFindCommand(leftParenNode[this.MQ.L]);
649
+ }
650
+
651
+ _selectNode(node, cursor) {
652
+ cursor.insLeftOf(node);
653
+ cursor.startSelection();
654
+ cursor.insRightOf(node);
655
+ cursor.select();
656
+ cursor.endSelection();
657
+ }
658
+
659
+ _isFraction(node) {
660
+ return node.jQ && node.jQ.hasClass("mq-fraction");
661
+ }
662
+
663
+ _isNumerator(node) {
664
+ return node.jQ && node.jQ.hasClass("mq-numerator");
665
+ }
666
+
667
+ _isDenominator(node) {
668
+ return node.jQ && node.jQ.hasClass("mq-denominator");
669
+ }
670
+
671
+ _isSubScript(node) {
672
+ // NOTE(charlie): MyScript has a structure whereby its superscripts seem
673
+ // to be represented as a parent node with 'mq-sup-only' containing a
674
+ // single child with 'mq-sup'.
675
+ return (
676
+ node.jQ &&
677
+ (node.jQ.hasClass("mq-sub-only") || node.jQ.hasClass("mq-sub"))
678
+ );
679
+ }
680
+
681
+ _isSuperScript(node) {
682
+ // NOTE(charlie): MyScript has a structure whereby its superscripts seem
683
+ // to be represented as a parent node with 'mq-sup-only' containing a
684
+ // single child with 'mq-sup'.
685
+ return (
686
+ node.jQ &&
687
+ (node.jQ.hasClass("mq-sup-only") || node.jQ.hasClass("mq-sup"))
688
+ );
689
+ }
690
+
691
+ _isParens(node) {
692
+ return node && node.ctrlSeq === "\\left(";
693
+ }
694
+
695
+ _isLeaf(node) {
696
+ return (
697
+ node && node.ctrlSeq && ValidLeaves.includes(node.ctrlSeq.trim())
698
+ );
699
+ }
700
+
701
+ _isSquareRoot(node) {
702
+ return (
703
+ node.blocks &&
704
+ node.blocks[0].jQ &&
705
+ node.blocks[0].jQ.hasClass("mq-sqrt-stem")
706
+ );
707
+ }
708
+
709
+ _isNthRoot(node) {
710
+ return (
711
+ node.blocks &&
712
+ node.blocks[0].jQ &&
713
+ node.blocks[0].jQ.hasClass("mq-nthroot")
714
+ );
715
+ }
716
+
717
+ _isNthRootIndex(node) {
718
+ return node.jQ && node.jQ.hasClass("mq-nthroot");
719
+ }
720
+
721
+ _isInsideLogIndex(cursor) {
722
+ const grandparent = cursor.parent.parent;
723
+
724
+ if (grandparent && grandparent.jQ.hasClass("mq-supsub")) {
725
+ const command = this._maybeFindCommandBeforeParens(grandparent);
726
+
727
+ if (command && command.name === "\\log") {
728
+ return true;
729
+ }
730
+ }
731
+
732
+ return false;
733
+ }
734
+
735
+ _isInsideEmptyNode(cursor) {
736
+ return cursor[this.MQ.L] === MQ_END && cursor[this.MQ.R] === MQ_END;
737
+ }
738
+
739
+ _handleBackspaceInRootIndex(cursor) {
740
+ if (this._isInsideEmptyNode(cursor)) {
741
+ // When deleting the index in a nthroot, we change from the nthroot
742
+ // to a sqrt, e.g. \sqrt[|]{35x-5} => |\sqrt{35x-5}. If there's no
743
+ // content under the root, then we delete the whole thing.
744
+
745
+ const grandparent = cursor.parent.parent;
746
+ const latex = grandparent.latex();
747
+ const reinsertionPoint = grandparent[this.MQ.L];
748
+
749
+ this._selectNode(grandparent, cursor);
750
+
751
+ const rootIsEmpty = grandparent.blocks[1].jQ.text() === "";
752
+
753
+ if (rootIsEmpty) {
754
+ // If there is not content under the root then simply delete
755
+ // the whole thing.
756
+ this.mathField.keystroke("Backspace");
757
+ } else {
758
+ // Replace the nthroot with a sqrt if there was content under
759
+ // the root.
760
+
761
+ // Start by deleting the selection.
762
+ this.mathField.keystroke("Backspace");
763
+
764
+ // Replace the nth-root with a sqrt.
765
+ this.mathField.write(latex.replace(/^\\sqrt\[\]/, "\\sqrt"));
766
+
767
+ // Adjust the cursor to be to the left the sqrt.
768
+ if (reinsertionPoint === MQ_END) {
769
+ this.mathField.moveToDirEnd(this.MQ.L);
770
+ } else {
771
+ cursor.insRightOf(reinsertionPoint);
772
+ }
773
+ }
774
+ } else {
775
+ if (cursor[this.MQ.L] !== MQ_END) {
776
+ // If the cursor is not at the leftmost position inside the
777
+ // root's index, delete a character.
778
+ this.mathField.keystroke("Backspace");
779
+ } else {
780
+ // TODO(kevinb) verify that we want this behavior after testing
781
+ // Do nothing because we haven't completely deleted the
782
+ // index of the radical.
783
+ }
784
+ }
785
+ }
786
+
787
+ _handleBackspaceInLogIndex(cursor) {
788
+ if (this._isInsideEmptyNode(cursor)) {
789
+ const grandparent = cursor.parent.parent;
790
+ const command = this._maybeFindCommandBeforeParens(grandparent);
791
+
792
+ cursor.insLeftOf(command.startNode);
793
+ cursor.startSelection();
794
+
795
+ if (grandparent[this.MQ.R] !== MQ_END) {
796
+ cursor.insRightOf(grandparent[this.MQ.R]);
797
+ } else {
798
+ cursor.insRightOf(grandparent);
799
+ }
800
+
801
+ cursor.select();
802
+ cursor.endSelection();
803
+
804
+ const isLogBodyEmpty =
805
+ grandparent[this.MQ.R].contentjQ.text() === "";
806
+
807
+ if (isLogBodyEmpty) {
808
+ // If there's no content inside the log's parens then delete the
809
+ // whole thing.
810
+ this.mathField.keystroke("Backspace");
811
+ }
812
+ } else {
813
+ this.mathField.keystroke("Backspace");
814
+ }
815
+ }
816
+
817
+ _handleBackspaceOutsideParens(cursor) {
818
+ // In this case the node with '\\left(' for its ctrlSeq
819
+ // is the parent of the expression contained within the
820
+ // parentheses.
821
+ //
822
+ // Handle selecting an expression before deleting:
823
+ // (x+1)| => |(x+1)|
824
+ // \log(x+1)| => |\log(x+1)|
825
+
826
+ const leftNode = cursor[this.MQ.L];
827
+ const rightNode = cursor[this.MQ.R];
828
+ const command = this._maybeFindCommandBeforeParens(leftNode);
829
+
830
+ if (command && command.startNode) {
831
+ // There's a command before the parens so we select it as well as
832
+ // the parens.
833
+ cursor.insLeftOf(command.startNode);
834
+ cursor.startSelection();
835
+ if (rightNode === MQ_END) {
836
+ cursor.insAtRightEnd(cursor.parent);
837
+ } else {
838
+ cursor.insLeftOf(rightNode);
839
+ }
840
+ cursor.select();
841
+ cursor.endSelection();
842
+ } else {
843
+ cursor.startSelection();
844
+ cursor.insLeftOf(leftNode); // left of \\left(
845
+ cursor.select();
846
+ cursor.endSelection();
847
+ }
848
+ }
849
+
850
+ _handleBackspaceInsideParens(cursor) {
851
+ // Handle situations when the cursor is inside parens or a
852
+ // command that uses parens, e.g. \log() or \tan()
853
+ //
854
+ // MathQuill represents log(x+1) in roughly the following way
855
+ // [l, o, g, \\left[parent:[x, +, 1]]]
856
+ //
857
+ // If the cursor is inside the parentheses it's next to one of:
858
+ // x, +, or 1. This makes sub_sub_expr its parent and sub_expr
859
+ // it's parent.
860
+ //
861
+ // Interestingly parent doesn't have any nodes to the left or
862
+ // right of it (even though the corresponding DOM node has
863
+ // ( and ) characters on either side.
864
+ //
865
+ // The grandparent's ctrlSeq is `\\left(`. The `\\right)` isn't
866
+ // stored anywhere. NOTE(kevinb): I believe this is because
867
+ // MathQuill knows what the close paren should be and does the
868
+ // right thing at render time.
869
+ //
870
+ // This conditional branch handles the following cases:
871
+ // - \log(x+1|) => \log(x+|)
872
+ // - \log(|x+1) => |\log(x+1)|
873
+ // - \log(|) => |
874
+
875
+ if (cursor[this.MQ.L] !== MQ_END) {
876
+ // This command contains math and there's some math to
877
+ // the left of the cursor that we should delete normally
878
+ // before doing anything special.
879
+ this.mathField.keystroke("Backspace");
880
+ return;
881
+ }
882
+
883
+ const grandparent = cursor.parent.parent;
884
+
885
+ // If the cursors is inside the parens at the start but the command
886
+ // has a subscript as is the case in log_n then move the cursor into
887
+ // the subscript, e.g. \log_{5}(|x+1) => \log_{5|}(x+1)
888
+
889
+ if (grandparent[this.MQ.L].sub) {
890
+ // if there is a subscript
891
+ if (grandparent[this.MQ.L].sub.jQ.text()) {
892
+ // and it contains text
893
+ // move the cursor to the right end of the subscript
894
+ cursor.insAtRightEnd(grandparent[this.MQ.L].sub);
895
+ return;
896
+ }
897
+ }
898
+
899
+ // Determine if the parens are empty before we modify the
900
+ // cursor's position.
901
+ const isEmpty = this._isInsideEmptyNode(cursor);
902
+
903
+ // Insert the cursor to the left of the command if there is one
904
+ // or before the '\\left(` if there isn't
905
+ const command = this._maybeFindCommandBeforeParens(grandparent);
906
+
907
+ cursor.insLeftOf((command && command.startNode) || grandparent);
908
+ cursor.startSelection();
909
+ cursor.insRightOf(grandparent);
910
+ cursor.select();
911
+ cursor.endSelection();
912
+
913
+ // Delete the selection, but only if the parens were empty to
914
+ // begin with.
915
+ if (isEmpty) {
916
+ this.mathField.keystroke("Backspace");
917
+ }
918
+ }
919
+
920
+ _handleBackspaceAfterLigaturedSymbol(cursor) {
921
+ this.mathField.keystroke("Backspace");
922
+ this.mathField.keystroke("Backspace");
923
+ }
924
+
925
+ contextForCursor(cursor) {
926
+ // First, try to find any fraction to the right, unimpeded.
927
+ let visitor = cursor;
928
+ while (visitor[this.MQ.R] !== MQ_END) {
929
+ if (this._isFraction(visitor[this.MQ.R])) {
930
+ return CursorContexts.BEFORE_FRACTION;
931
+ } else if (!this._isLeaf(visitor[this.MQ.R])) {
932
+ break;
933
+ }
934
+ visitor = visitor[this.MQ.R];
935
+ }
936
+
937
+ // If that didn't work, check if the parent or grandparent is a special
938
+ // context, so that we can jump outwards.
939
+ if (this._isParens(cursor.parent && cursor.parent.parent)) {
940
+ return CursorContexts.IN_PARENS;
941
+ } else if (this._isNumerator(cursor.parent)) {
942
+ return CursorContexts.IN_NUMERATOR;
943
+ } else if (this._isDenominator(cursor.parent)) {
944
+ return CursorContexts.IN_DENOMINATOR;
945
+ } else if (this._isSubScript(cursor.parent)) {
946
+ return CursorContexts.IN_SUB_SCRIPT;
947
+ } else if (this._isSuperScript(cursor.parent)) {
948
+ return CursorContexts.IN_SUPER_SCRIPT;
949
+ } else {
950
+ return CursorContexts.NONE;
951
+ }
952
+ }
953
+
954
+ _isAtTopLevel(cursor) {
955
+ return !cursor.parent.parent;
956
+ }
957
+ }
958
+
959
+ export default MathWrapper;