@pie-lib/math-input-mathlive 1.1.4 → 1.1.5-beta.1

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 (43) hide show
  1. package/README.md +14 -5
  2. package/lib/fonts/embedded.js +120 -0
  3. package/lib/fonts/index.js +81 -0
  4. package/lib/fonts/index.js.map +1 -0
  5. package/lib/keypad/index.js +15 -17
  6. package/lib/keypad/index.js.map +1 -1
  7. package/lib/keys/geometry.js +7 -4
  8. package/lib/keys/geometry.js.map +1 -1
  9. package/lib/keys/misc.js +9 -2
  10. package/lib/keys/misc.js.map +1 -1
  11. package/lib/latex-bridge.js +48 -3
  12. package/lib/latex-bridge.js.map +1 -1
  13. package/lib/mathlive-instance.js +186 -15
  14. package/lib/mathlive-instance.js.map +1 -1
  15. package/lib/mf/common-styles.js +74 -10
  16. package/lib/mf/common-styles.js.map +1 -1
  17. package/lib/mf/index.js +6 -0
  18. package/lib/mf/index.js.map +1 -1
  19. package/lib/mf/input.js +29 -5
  20. package/lib/mf/input.js.map +1 -1
  21. package/lib/mf/static.js +105 -7
  22. package/lib/mf/static.js.map +1 -1
  23. package/package.json +3 -4
  24. package/scripts/generate-embedded-fonts.js +94 -0
  25. package/src/fonts/embedded.js +116 -0
  26. package/src/fonts/index.js +71 -0
  27. package/src/keypad/index.jsx +15 -14
  28. package/src/keys/geometry.js +7 -2
  29. package/src/keys/misc.js +8 -2
  30. package/src/latex-bridge.js +52 -3
  31. package/src/mathlive-instance.js +201 -14
  32. package/src/mf/common-styles.js +78 -8
  33. package/src/mf/index.js +2 -0
  34. package/src/mf/input.jsx +37 -5
  35. package/src/mf/static.jsx +121 -6
  36. package/CHANGELOG.md +0 -30
  37. package/LICENSE.md +0 -5
  38. package/src/__tests__/drop-in-compat.test.jsx +0 -112
  39. package/src/__tests__/index.test.jsx +0 -109
  40. package/src/__tests__/input-imperative.test.jsx +0 -144
  41. package/src/__tests__/keypad-label.test.jsx +0 -57
  42. package/src/__tests__/latex-bridge.test.js +0 -230
  43. package/src/__tests__/mathlive-instance.test.js +0 -181
package/src/mf/input.jsx CHANGED
@@ -2,7 +2,13 @@ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { styled } from '@mui/material/styles';
4
4
  import debug from 'debug';
5
- import { loadMathLive, getMacros } from '../mathlive-instance';
5
+ import {
6
+ loadMathLive,
7
+ getMacros,
8
+ MATH_MODE_SPACE,
9
+ applyShadowStyles,
10
+ trackPlaceholderCaret,
11
+ } from '../mathlive-instance';
6
12
  import { toMathLive, fromMathLive, keyToAction, KEYSTROKES } from '../latex-bridge';
7
13
 
8
14
  const log = debug('pie-lib:math-input-mathlive:input');
@@ -49,9 +55,16 @@ export class Input extends React.Component {
49
55
  }
50
56
 
51
57
  async componentDidMount() {
58
+ // MathLive loads asynchronously, so this method resumes after an await and
59
+ // has to know whether it is still the current mount. React StrictMode calls
60
+ // mount -> unmount -> mount on the SAME instance in dev, so a boolean
61
+ // "unmounted" flag would latch and block the remount from ever creating a
62
+ // field. A generation counter identifies the live mount instead: only the
63
+ // newest one proceeds, and a real unmount invalidates all pending ones.
64
+ const generation = (this.mountGeneration = (this.mountGeneration || 0) + 1);
52
65
  const ml = await loadMathLive();
53
66
 
54
- if (!ml || !this.holderRef.current) {
67
+ if (generation !== this.mountGeneration || !ml || !this.holderRef.current || this.mathField) {
55
68
  return;
56
69
  }
57
70
 
@@ -64,15 +77,25 @@ export class Input extends React.Component {
64
77
 
65
78
  // pie drives input through its own keypad, so suppress MathLive's.
66
79
  this.mathField.mathVirtualKeyboardPolicy = 'manual';
80
+ // Without this the spacebar does nothing: MathLive's mathModeSpace
81
+ // defaults to an empty string.
82
+ this.mathField.mathModeSpace = MATH_MODE_SPACE;
83
+
84
+ // The mathfield renders in shadow DOM, so page CSS cannot reach it - the
85
+ // stretchy-accent fix has to be injected there directly.
86
+ applyShadowStyles(this.mathField);
87
+
88
+ // A selected `\placeholder{}` gets no MathLive caret; draw one.
89
+ this.untrackCaret = trackPlaceholderCaret(this.mathField);
67
90
 
68
91
  this.mathField.addEventListener('input', this.onInputEdit);
69
- this.holderRef.current.appendChild(this.mathField);
92
+
93
+ // Belt and braces: drop anything a previous mount may have left behind.
94
+ this.holderRef.current.replaceChildren(this.mathField);
70
95
 
71
96
  if (this.props.innerRef) {
72
97
  this.props.innerRef(this);
73
98
  }
74
-
75
- this.setState?.({});
76
99
  }
77
100
 
78
101
  componentDidUpdate(prevProps) {
@@ -99,6 +122,15 @@ export class Input extends React.Component {
99
122
  }
100
123
 
101
124
  componentWillUnmount() {
125
+ // Invalidate any mount still awaiting the MathLive load, so it does not
126
+ // append a field into a holder that is going away.
127
+ this.mountGeneration = (this.mountGeneration || 0) + 1;
128
+
129
+ if (this.untrackCaret) {
130
+ this.untrackCaret();
131
+ this.untrackCaret = undefined;
132
+ }
133
+
102
134
  if (this.mathField) {
103
135
  this.mathField.removeEventListener('input', this.onInputEdit);
104
136
  this.mathField.remove();
package/src/mf/static.jsx CHANGED
@@ -2,9 +2,16 @@ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { styled } from '@mui/material/styles';
4
4
  import debug from 'debug';
5
- import { loadMathLive, getMacros, latexToMarkup } from '../mathlive-instance';
5
+ import {
6
+ loadMathLive,
7
+ getMacros,
8
+ latexToMarkup,
9
+ MATH_MODE_SPACE,
10
+ applyShadowStyles,
11
+ trackPlaceholderCaret,
12
+ } from '../mathlive-instance';
6
13
  import { toMathLive, fromMathLive, fieldIds } from '../latex-bridge';
7
- import { placeholderStyles } from './common-styles';
14
+ import { placeholderStyles, rootIndexStyles } from './common-styles';
8
15
 
9
16
  const log = debug('pie-lib:math-input-mathlive:static');
10
17
 
@@ -12,6 +19,9 @@ const Holder = styled('span')({
12
19
  display: 'inline-block',
13
20
  // Shared static-math rules: muted empty-slot glyphs and svg containment.
14
21
  ...placeholderStyles,
22
+ // Readable radical index - display mode renders in the light DOM, so the
23
+ // shadow-root copy of this rule does not reach it.
24
+ ...rootIndexStyles,
15
25
  '& math-field': {
16
26
  display: 'inline-block',
17
27
  border: 'none',
@@ -64,28 +74,49 @@ export default class Static extends React.Component {
64
74
  }
65
75
 
66
76
  async componentDidMount() {
77
+ // See the note in mf/input.jsx: a generation counter, not a boolean, so a
78
+ // StrictMode mount -> unmount -> mount on the same instance still ends up
79
+ // with exactly one field.
80
+ const generation = (this.mountGeneration = (this.mountGeneration || 0) + 1);
81
+
67
82
  if (!this.isInteractive()) {
68
83
  await loadMathLive();
69
- this.renderMarkup();
84
+
85
+ if (generation === this.mountGeneration) {
86
+ this.renderMarkup();
87
+ }
88
+
70
89
  return;
71
90
  }
72
91
 
73
92
  const ml = await loadMathLive();
74
93
 
75
- if (!ml || !this.holderRef.current) {
94
+ if (generation !== this.mountGeneration || !ml || !this.holderRef.current || this.mathField) {
76
95
  return;
77
96
  }
78
97
 
79
98
  this.mathField = new ml.MathfieldElement({ macros: getMacros() });
80
99
  this.mathField.readOnly = true;
81
100
  this.mathField.mathVirtualKeyboardPolicy = 'manual';
101
+ // Without this the spacebar does nothing: MathLive's mathModeSpace
102
+ // defaults to an empty string.
103
+ this.mathField.mathModeSpace = MATH_MODE_SPACE;
82
104
  this.mathField.value = toMathLive(this.props.latex);
83
105
 
106
+ // The mathfield renders in shadow DOM, so page CSS cannot reach it - the
107
+ // stretchy-accent fix has to be injected there directly.
108
+ applyShadowStyles(this.mathField);
109
+
110
+ // A selected `\placeholder{}` gets no MathLive caret; draw one. Answer
111
+ // blocks are typed into with the same keypad, so they grow placeholders too.
112
+ this.untrackCaret = trackPlaceholderCaret(this.mathField);
113
+
84
114
  this.mathField.addEventListener('input', this.onPromptInput);
85
115
  this.mathField.addEventListener('focusin', this.onPromptFocus);
86
116
  this.mathField.addEventListener('keydown', this.onKeyDown);
87
117
 
88
- this.holderRef.current.appendChild(this.mathField);
118
+ // Belt and braces: drop anything a previous mount may have left behind.
119
+ this.holderRef.current.replaceChildren(this.mathField);
89
120
 
90
121
  this.createLiveRegion();
91
122
  }
@@ -103,6 +134,14 @@ export default class Static extends React.Component {
103
134
  }
104
135
 
105
136
  componentWillUnmount() {
137
+ // Invalidate any mount still awaiting the MathLive load.
138
+ this.mountGeneration = (this.mountGeneration || 0) + 1;
139
+
140
+ if (this.untrackCaret) {
141
+ this.untrackCaret();
142
+ this.untrackCaret = undefined;
143
+ }
144
+
106
145
  if (this.mathField) {
107
146
  this.mathField.removeEventListener('input', this.onPromptInput);
108
147
  this.mathField.removeEventListener('focusin', this.onPromptFocus);
@@ -234,6 +273,13 @@ export default class Static extends React.Component {
234
273
  promptHandle(id) {
235
274
  return {
236
275
  id,
276
+ // MathQuill's inner fields could be focused directly; keep that.
277
+ focus: () => {
278
+ if (this.mathField) {
279
+ this.mathField.focus();
280
+ this.focusPrompt(id);
281
+ }
282
+ },
237
283
  latex: (value) => {
238
284
  if (!this.mathField) {
239
285
  return '';
@@ -331,8 +377,77 @@ export default class Static extends React.Component {
331
377
  }
332
378
  };
333
379
 
380
+ /** The id of the prompt whose range contains `position`, if any. */
381
+ promptContaining(position) {
382
+ if (!this.mathField || typeof position !== 'number') {
383
+ return undefined;
384
+ }
385
+
386
+ return (this.mathField.getPrompts() || []).find((id) => {
387
+ const range = this.mathField.getPromptRange(id);
388
+
389
+ return range && position >= range[0] && position <= range[1];
390
+ });
391
+ }
392
+
393
+ /**
394
+ * Put a blinking caret inside an answer block.
395
+ *
396
+ * MathLive only makes the caret visible while the field element carries the
397
+ * `ML__focused` class, and it adds that class during a *render*, gated on
398
+ * `isSelectionEditable && hasFocus()`. In a read-only field only prompt
399
+ * interiors are editable, so the two things both have to be true:
400
+ *
401
+ * - the selection must sit inside a prompt, and
402
+ * - a render must run afterwards.
403
+ *
404
+ * Assigning `selection` satisfies the first but not the second - it updates
405
+ * the model without re-rendering, so the class stays off and the caret span
406
+ * remains `visibility: hidden`. Executing any command does trigger the
407
+ * render, and `scrollIntoView` is the one with no other effect: it keeps the
408
+ * caret on screen, which the preceding `focus()` already does anyway.
409
+ *
410
+ * @param {string} [id] prompt to focus; defaults to the first one
411
+ */
412
+ focusPrompt(id) {
413
+ if (!this.mathField) {
414
+ return;
415
+ }
416
+
417
+ const ids = this.mathField.getPrompts() || [];
418
+ const target = id && ids.indexOf(id) !== -1 ? id : ids[0];
419
+
420
+ if (!target) {
421
+ return;
422
+ }
423
+
424
+ const range = this.mathField.getPromptRange(target);
425
+
426
+ if (!range) {
427
+ return;
428
+ }
429
+
430
+ this.mathField.selection = { ranges: [[range[0], range[0]]] };
431
+
432
+ if (typeof this.mathField.executeCommand === 'function') {
433
+ this.mathField.executeCommand('scrollIntoView');
434
+ }
435
+ }
436
+
334
437
  focus() {
335
- this.mathField && this.mathField.focus();
438
+ if (!this.mathField) {
439
+ return;
440
+ }
441
+
442
+ this.mathField.focus();
443
+
444
+ // A bare focus() leaves the selection outside every prompt, which in a
445
+ // read-only field is not editable - the block looked focused but had no
446
+ // cursor. Clicking a prompt already works, because MathLive's own pointer
447
+ // handling moves the selection and re-renders.
448
+ if (!this.promptContaining(this.mathField.position)) {
449
+ this.focusPrompt();
450
+ }
336
451
  }
337
452
 
338
453
  blur() {
package/CHANGELOG.md DELETED
@@ -1,30 +0,0 @@
1
- # Change Log
2
-
3
- All notable changes to this project will be documented in this file.
4
- See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
-
6
- ## [1.1.4](https://github.com/pie-framework/pie-lib/compare/@pie-lib/math-input-mathlive@1.1.3...@pie-lib/math-input-mathlive@1.1.4) (2026-09-22)
7
-
8
- **Note:** Version bump only for package @pie-lib/math-input-mathlive
9
-
10
- ## [1.1.3](https://github.com/pie-framework/pie-lib/compare/@pie-lib/math-input-mathlive@1.1.2...@pie-lib/math-input-mathlive@1.1.3) (2026-09-02)
11
-
12
- **Note:** Version bump only for package @pie-lib/math-input-mathlive
13
-
14
- ## [1.1.2](https://github.com/pie-framework/pie-lib/compare/@pie-lib/math-input-mathlive@1.1.1...@pie-lib/math-input-mathlive@1.1.2) (2026-08-26)
15
-
16
- **Note:** Version bump only for package @pie-lib/math-input-mathlive
17
-
18
- ## [1.1.1](https://github.com/pie-framework/pie-lib/compare/@pie-lib/math-input-mathlive@1.1.0...@pie-lib/math-input-mathlive@1.1.1) (2026-08-21)
19
-
20
- **Note:** Version bump only for package @pie-lib/math-input-mathlive
21
-
22
- # 1.1.0 (2026-08-13)
23
-
24
- ### Bug Fixes
25
-
26
- - bump math-input-mathlive version to allow canary workflow to generate next on develop ([1ae0088](https://github.com/pie-framework/pie-lib/commit/1ae0088e3b8985aa65933d6a35d94f609cfb9420))
27
-
28
- ### Features
29
-
30
- - implemented math-input-mathlive [PIE-158] ([0d0aa07](https://github.com/pie-framework/pie-lib/commit/0d0aa07cd946b758cb280add754615690542eca3))
package/LICENSE.md DELETED
@@ -1,5 +0,0 @@
1
- Copyright 2019 CoreSpring Inc
2
-
3
- Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
4
-
5
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -1,112 +0,0 @@
1
- import React from 'react';
2
- import { render } from '@testing-library/react';
3
- import { styled } from '@mui/material/styles';
4
-
5
- import { mq } from '../index';
6
-
7
- /**
8
- * Exercises the exact call patterns real consumers use against
9
- * @pie-lib/math-input, to catch drop-in incompatibilities.
10
- *
11
- * Sources:
12
- * - packages/math-toolbar/src/editor-and-pad.jsx:16,185
13
- * - packages/math-toolbar/src/math-preview.jsx:8,156
14
- * - packages/editable-html/src/plugins/respArea/math-templated/index.jsx:92
15
- * - packages/editable-html-tip-tap/src/components/respArea/MathTemplated.jsx:111
16
- */
17
- describe('drop-in compatibility with @pie-lib/math-input consumers', () => {
18
- describe('mq.CommonMqStyles destructuring', () => {
19
- it('math-preview.jsx: { commonMqFontStyles, longdivStyles, supsubStyles }', () => {
20
- const { commonMqFontStyles, longdivStyles, supsubStyles } = mq.CommonMqStyles;
21
-
22
- expect(commonMqFontStyles).toBeDefined();
23
- expect(longdivStyles).toBeDefined();
24
- expect(supsubStyles).toBeDefined();
25
- });
26
-
27
- it('editor-and-pad.jsx: also { commonMqKeyboardStyles }', () => {
28
- const { commonMqFontStyles, commonMqKeyboardStyles, longdivStyles, supsubStyles } = mq.CommonMqStyles;
29
-
30
- expect(commonMqKeyboardStyles).toBeDefined();
31
- // All four must be spreadable into an emotion style object.
32
- expect(() => ({
33
- ...commonMqFontStyles,
34
- ...commonMqKeyboardStyles,
35
- ...longdivStyles,
36
- ...supsubStyles,
37
- })).not.toThrow();
38
- });
39
-
40
- it('the destructured groups survive being spread into a styled component', () => {
41
- const { commonMqFontStyles, longdivStyles, supsubStyles } = mq.CommonMqStyles;
42
- const Styled = styled('div')({ ...commonMqFontStyles, ...longdivStyles, ...supsubStyles });
43
-
44
- expect(() => render(<Styled />)).not.toThrow();
45
- });
46
- });
47
-
48
- describe('mq.Input', () => {
49
- it('editor-and-pad.jsx: styled(mq.Input) renders', () => {
50
- const MathEditor = styled(mq.Input)(() => ({ width: '100%' }));
51
-
52
- expect(() => render(<MathEditor latex="\\frac{1}{2}" onChange={() => {}} />)).not.toThrow();
53
- });
54
-
55
- it('accepts the className that styled() injects', () => {
56
- const Styled = styled(mq.Input)({ padding: 4 });
57
- const { container } = render(<Styled latex="x" onChange={() => {}} />);
58
-
59
- expect(container.firstChild).toBeTruthy();
60
- expect(container.firstChild.className).toBeTruthy();
61
- });
62
- });
63
-
64
- describe('mq.Static', () => {
65
- it('math-preview.jsx: <mq.Static latex onFocus onBlur />', () => {
66
- expect(() => render(<mq.Static latex={'\\frac{1}{2}'} onFocus={() => {}} onBlur={() => {}} />)).not.toThrow();
67
- });
68
-
69
- it('math-templated respArea: <mq.Static latex={value} />', () => {
70
- expect(() => render(<mq.Static latex={'x=1'} />)).not.toThrow();
71
- });
72
-
73
- it('renders answer-block latex (the templated-math case) without throwing', () => {
74
- expect(() =>
75
- render(<mq.Static latex={'x=\\MathQuillMathField[r1]{}'} onSubFieldChange={() => {}} />),
76
- ).not.toThrow();
77
- });
78
-
79
- it('is styleable, as keypad labels were in math-input', () => {
80
- const Styled = styled(mq.Static)({ pointerEvents: 'none' });
81
-
82
- expect(() => render(<Styled latex={'\\pi'} />)).not.toThrow();
83
- });
84
- });
85
-
86
- describe('surface parity', () => {
87
- it('exposes exactly the names math-input exported', () => {
88
- // eslint-disable-next-line global-require
89
- const pkg = require('../index');
90
-
91
- [
92
- 'keysForGrade',
93
- 'addBrackets',
94
- 'removeBrackets',
95
- 'keys',
96
- 'HorizontalKeypad',
97
- 'mq',
98
- 'updateSpans',
99
- 'registerEmbed',
100
- 'applyStaticMath',
101
- ].forEach((name) => {
102
- expect(pkg[name]).toBeDefined();
103
- });
104
- });
105
-
106
- it('mq exposes Input, Static and CommonMqStyles', () => {
107
- ['Input', 'Static', 'CommonMqStyles'].forEach((name) => {
108
- expect(mq[name]).toBeDefined();
109
- });
110
- });
111
- });
112
- });
@@ -1,109 +0,0 @@
1
- import React from 'react';
2
- import { render } from '@testing-library/react';
3
-
4
- import {
5
- keys,
6
- keysForGrade,
7
- addBrackets,
8
- removeBrackets,
9
- registerEmbed,
10
- applyStaticMath,
11
- updateSpans,
12
- mf,
13
- mq,
14
- MathInput,
15
- HorizontalKeypad,
16
- KeyPad,
17
- } from '../index';
18
-
19
- describe('math-input-mathlive public API', () => {
20
- it('mirrors the @pie-lib/math-input export surface', () => {
21
- expect(typeof keysForGrade).toBe('function');
22
- expect(typeof addBrackets).toBe('function');
23
- expect(typeof removeBrackets).toBe('function');
24
- expect(typeof registerEmbed).toBe('function');
25
- expect(typeof applyStaticMath).toBe('function');
26
- expect(typeof updateSpans).toBe('function');
27
- expect(keys).toBeDefined();
28
- expect(keys.baseSet).toBeDefined();
29
- });
30
-
31
- it('exports the field wrappers under both mf and mq', () => {
32
- expect(mf.Input).toBeDefined();
33
- expect(mf.Static).toBeDefined();
34
- // `mq` alias lets a consumer swap packages by changing only the import.
35
- expect(mq).toBe(mf);
36
- });
37
-
38
- it('updateSpans is a safe no-op', () => {
39
- expect(() => updateSpans()).not.toThrow();
40
- });
41
-
42
- it('CommonMqStyles keeps the shape consumers destructure', () => {
43
- // math-toolbar does:
44
- // const { commonMqFontStyles, commonMqKeyboardStyles, longdivStyles, supsubStyles }
45
- // = mq.CommonMqStyles;
46
- const { commonMqFontStyles, commonMqKeyboardStyles, longdivStyles, supsubStyles } = mf.CommonMqStyles;
47
-
48
- [commonMqFontStyles, commonMqKeyboardStyles, longdivStyles, supsubStyles].forEach((s) => {
49
- expect(s).toBeDefined();
50
- expect(typeof s).toBe('object');
51
- });
52
- });
53
-
54
- it('bracket helpers round-trip', () => {
55
- expect(addBrackets('x')).toEqual('\\(x\\)');
56
- expect(removeBrackets('\\(x\\)')).toEqual('x');
57
- expect(removeBrackets(addBrackets('\\frac{1}{2}'))).toEqual('\\frac{1}{2}');
58
- });
59
-
60
- it('keysForGrade returns a 5-row keyset', () => {
61
- const set = keysForGrade('geometry');
62
-
63
- expect(Array.isArray(set)).toBe(true);
64
- expect(set.length).toBe(5);
65
- });
66
- });
67
-
68
- describe('rendering', () => {
69
- // MathLive is a browser-only custom element; loadMathLive() resolves async and
70
- // is a no-op-ish under jsdom. These assert the components mount without
71
- // throwing, which is what regressed most often in the MathQuill version.
72
- it('KeyPad renders without a live MathLive instance', () => {
73
- const { container } = render(<KeyPad onPress={() => {}} />);
74
-
75
- expect(container.querySelectorAll('button').length).toBeGreaterThan(0);
76
- });
77
-
78
- it('KeyPad falls back to raw latex for labels before MathLive loads', () => {
79
- const { container } = render(
80
- <KeyPad onPress={() => {}} baseSet={[[{ name: 'pi', latex: '\\pi', category: 'constants' }]]} />,
81
- );
82
-
83
- expect(container.textContent).toContain('\\pi');
84
- });
85
-
86
- it('KeyPad fires onPress with the key definition', () => {
87
- const onPress = jest.fn();
88
- const key = { name: 'x', label: 'x', write: 'x', category: 'vars' };
89
- const { container } = render(<KeyPad onPress={onPress} baseSet={[[key]]} />);
90
-
91
- container.querySelector('button').click();
92
-
93
- expect(onPress).toHaveBeenCalledWith(key);
94
- });
95
-
96
- it('MathInput mounts', () => {
97
- expect(() => render(<MathInput latex="\\frac{1}{2}" onChange={() => {}} />)).not.toThrow();
98
- });
99
-
100
- it('HorizontalKeypad mounts and reports the legacy {value,type} model', () => {
101
- const onClick = jest.fn();
102
-
103
- expect(() => render(<HorizontalKeypad onClick={onClick} />)).not.toThrow();
104
- });
105
-
106
- it('Static renders display-mode latex without a mathfield', () => {
107
- expect(() => render(<mf.Static latex="\\frac{1}{2}" />)).not.toThrow();
108
- });
109
- });
@@ -1,144 +0,0 @@
1
- import { KEYSTROKES, keyToAction } from '../latex-bridge';
2
- import Input from '../mf/input';
3
-
4
- /**
5
- * The imperative surface that @pie-lib/math-toolbar drives
6
- * (editor-and-pad.jsx: clear/command/keystroke/write/latex).
7
- *
8
- * These call the methods against a stub mathfield, because a real
9
- * <math-field> needs a browser. What matters here is the *translation*:
10
- * MathQuill-era arguments must become valid MathLive operations.
11
- */
12
- const stubField = () => {
13
- const calls = { insert: [], executeCommand: [], focus: 0 };
14
-
15
- return {
16
- calls,
17
- value: '',
18
- getValue: () => '',
19
- insert(v) {
20
- calls.insert.push(v);
21
- },
22
- executeCommand(v) {
23
- calls.executeCommand.push(v);
24
- },
25
- focus() {
26
- calls.focus++;
27
- },
28
- blur() {},
29
- };
30
- };
31
-
32
- const withField = () => {
33
- const input = new Input({});
34
- const f = stubField();
35
-
36
- input.mathField = f;
37
-
38
- return { input, f };
39
- };
40
-
41
- describe('Input imperative API (drives math-toolbar)', () => {
42
- describe('keystroke', () => {
43
- it('maps MathQuill keystroke names to MathLive selectors', () => {
44
- const { input, f } = withField();
45
-
46
- input.keystroke('Left');
47
- input.keystroke('Right');
48
- input.keystroke('Backspace');
49
-
50
- expect(f.calls.executeCommand).toEqual(['moveToPreviousChar', 'moveToNextChar', 'deleteBackward']);
51
- });
52
-
53
- it('passes an unknown value through as a selector', () => {
54
- const { input, f } = withField();
55
-
56
- input.keystroke('deleteForward');
57
-
58
- expect(f.calls.executeCommand).toEqual(['deleteForward']);
59
- });
60
-
61
- it('does not throw when the engine rejects the selector', () => {
62
- const { input } = withField();
63
-
64
- input.mathField.executeCommand = () => {
65
- throw new Error('unknown command');
66
- };
67
-
68
- expect(() => input.keystroke('Nonsense')).not.toThrow();
69
- });
70
-
71
- it('covers every keystroke the keypad emits', () => {
72
- // navigation.js / edit.js only produce these three
73
- ['Left', 'Right', 'Backspace'].forEach((k) => expect(KEYSTROKES[k]).toBeDefined());
74
- });
75
- });
76
-
77
- describe('command', () => {
78
- it('expands argument-taking commands into placeholders', () => {
79
- const { input, f } = withField();
80
-
81
- input.command('\\frac');
82
-
83
- // a bare \frac would give no editable slots
84
- expect(f.calls.insert).toEqual(['\\frac{#?}{#?}']);
85
- });
86
-
87
- it('handles array commands', () => {
88
- const { input, f } = withField();
89
-
90
- input.command(['\\log', '_']);
91
-
92
- expect(f.calls.insert).toEqual(['\\log_{#?}']);
93
- });
94
-
95
- it('passes argument-less commands through', () => {
96
- const { input, f } = withField();
97
-
98
- input.command('\\pi');
99
-
100
- expect(f.calls.insert).toEqual(['\\pi']);
101
- });
102
-
103
- it('agrees with keyToAction', () => {
104
- expect(keyToAction({ command: '\\sqrt' }).value).toEqual('\\sqrt{#?}');
105
- });
106
- });
107
-
108
- describe('safety before MathLive loads', () => {
109
- it('every method is a no-op rather than a crash', () => {
110
- const input = new Input({});
111
-
112
- // mathField is undefined until loadMathLive() resolves
113
- expect(input.latex()).toEqual('');
114
- expect(input.clear()).toEqual('');
115
- expect(input.command('\\frac')).toEqual('');
116
- expect(input.keystroke('Left')).toEqual('');
117
- expect(input.write('x')).toEqual('');
118
- expect(() => input.focus()).not.toThrow();
119
- expect(() => input.blur()).not.toThrow();
120
- expect(() => input.setLatex('x')).not.toThrow();
121
- expect(input.element).toBeUndefined();
122
- });
123
- });
124
-
125
- describe('mqCompat facade', () => {
126
- // math-toolbar used to call this.input.mathField.latex(); external
127
- // consumers may still do the same.
128
- it('exposes latex() as a getter', () => {
129
- const { input } = withField();
130
-
131
- input.mathField.getValue = () => '\\frac{1}{2}';
132
-
133
- expect(input.mqCompat.latex()).toEqual('\\frac{1}{2}');
134
- });
135
-
136
- it('exposes latex(v) as a setter and el()', () => {
137
- const { input, f } = withField();
138
-
139
- input.mqCompat.latex('x+1');
140
- expect(f.value).toEqual('x+1');
141
- expect(input.mqCompat.el()).toBe(f);
142
- });
143
- });
144
- });