@pie-lib/math-input-mathlive 1.1.5-beta.8 → 2.0.1-next.20261107131445

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 (202) hide show
  1. package/README.md +84 -109
  2. package/dist/_virtual/_rolldown/runtime.js +11 -0
  3. package/dist/horizontal-keypad.d.ts +22 -0
  4. package/dist/horizontal-keypad.js +54 -0
  5. package/dist/index.d.ts +20 -0
  6. package/dist/index.js +12 -0
  7. package/dist/keypad/index.d.ts +40 -0
  8. package/dist/keypad/index.js +245 -0
  9. package/dist/keypad/keys-layout.d.ts +7 -0
  10. package/dist/keypad/keys-layout.js +5 -0
  11. package/dist/keys/basic-operators.d.ts +5 -0
  12. package/dist/keys/basic-operators.js +30 -0
  13. package/dist/keys/chars.d.ts +5 -0
  14. package/dist/keys/comparison.d.ts +4 -0
  15. package/dist/keys/comparison.js +32 -0
  16. package/dist/keys/constants.d.ts +4 -0
  17. package/dist/keys/constants.js +35 -0
  18. package/dist/keys/digits.d.ts +15 -0
  19. package/dist/keys/digits.js +34 -0
  20. package/dist/keys/edit.d.ts +6 -0
  21. package/dist/keys/edit.js +9 -0
  22. package/dist/keys/exponent.d.ts +4 -0
  23. package/dist/keys/exponent.js +28 -0
  24. package/dist/keys/fractions.d.ts +3 -0
  25. package/dist/keys/fractions.js +27 -0
  26. package/dist/keys/geometry.d.ts +23 -0
  27. package/dist/keys/geometry.js +127 -0
  28. package/dist/keys/grades.d.ts +9 -0
  29. package/dist/keys/grades.js +414 -0
  30. package/dist/keys/index.d.ts +6 -0
  31. package/dist/keys/index.js +50 -0
  32. package/dist/keys/log.d.ts +3 -0
  33. package/dist/keys/log.js +25 -0
  34. package/dist/keys/logic.d.ts +2 -0
  35. package/dist/keys/logic.js +13 -0
  36. package/dist/keys/matrices.d.ts +2 -0
  37. package/dist/keys/matrices.js +17 -0
  38. package/dist/keys/misc.d.ts +10 -0
  39. package/dist/keys/misc.js +60 -0
  40. package/dist/keys/navigation.d.ts +2 -0
  41. package/dist/keys/navigation.js +13 -0
  42. package/dist/keys/operators.d.ts +1 -0
  43. package/dist/keys/operators.js +11 -0
  44. package/dist/keys/statistics.d.ts +5 -0
  45. package/dist/keys/statistics.js +38 -0
  46. package/dist/keys/sub-sup.d.ts +2 -0
  47. package/dist/keys/sub-sup.js +17 -0
  48. package/dist/keys/trigonometry.d.ts +6 -0
  49. package/dist/keys/trigonometry.js +43 -0
  50. package/dist/keys/utils.d.ts +5 -0
  51. package/dist/keys/utils.js +24 -0
  52. package/dist/keys/vars.d.ts +3 -0
  53. package/dist/keys/vars.js +22 -0
  54. package/dist/latex-bridge.d.ts +197 -0
  55. package/dist/latex-bridge.js +210 -0
  56. package/dist/longdiv.d.ts +24 -0
  57. package/dist/longdiv.js +43 -0
  58. package/dist/math-input.d.ts +26 -0
  59. package/dist/math-input.js +74 -0
  60. package/dist/mathlive-instance.d.ts +221 -0
  61. package/dist/mathlive-instance.js +159 -0
  62. package/dist/mf/common-styles.d.ts +594 -0
  63. package/dist/mf/common-styles.js +80 -0
  64. package/dist/mf/field-label.d.ts +16 -0
  65. package/dist/mf/field-label.js +13 -0
  66. package/dist/mf/index.d.ts +4 -0
  67. package/dist/mf/index.js +21 -0
  68. package/dist/mf/input.d.ts +94 -0
  69. package/dist/mf/input.js +127 -0
  70. package/dist/mf/static.d.ts +143 -0
  71. package/dist/mf/static.js +230 -0
  72. package/dist/selection.d.ts +43 -0
  73. package/dist/selection.js +39 -0
  74. package/package.json +32 -18
  75. package/lib/__tests__/composites.test.js +0 -255
  76. package/lib/__tests__/drop-in-compat.test.js +0 -133
  77. package/lib/__tests__/embedded-fonts.test.js +0 -121
  78. package/lib/__tests__/index.test.js +0 -118
  79. package/lib/__tests__/input-imperative.test.js +0 -155
  80. package/lib/__tests__/input-selection.test.js +0 -103
  81. package/lib/__tests__/keypad-label.test.js +0 -231
  82. package/lib/__tests__/keypad-render.test.js +0 -206
  83. package/lib/__tests__/latex-bridge.test.js +0 -386
  84. package/lib/__tests__/mathlive-instance.test.js +0 -419
  85. package/lib/__tests__/mount-race.test.js +0 -218
  86. package/lib/__tests__/selection.test.js +0 -195
  87. package/lib/__tests__/static-prompts.test.js +0 -405
  88. package/lib/__tests__/tmp-markup.test.js +0 -26
  89. package/lib/horizontal-keypad.js +0 -101
  90. package/lib/horizontal-keypad.js.map +0 -1
  91. package/lib/index.js +0 -143
  92. package/lib/index.js.map +0 -1
  93. package/lib/keypad/index.js +0 -431
  94. package/lib/keypad/index.js.map +0 -1
  95. package/lib/keypad/keys-layout.js +0 -22
  96. package/lib/keypad/keys-layout.js.map +0 -1
  97. package/lib/keys/basic-operators.js +0 -33
  98. package/lib/keys/basic-operators.js.map +0 -1
  99. package/lib/keys/chars.js +0 -12
  100. package/lib/keys/chars.js.map +0 -1
  101. package/lib/keys/comparison.js +0 -39
  102. package/lib/keys/comparison.js.map +0 -1
  103. package/lib/keys/constants.js +0 -37
  104. package/lib/keys/constants.js.map +0 -1
  105. package/lib/keys/digits.js +0 -46
  106. package/lib/keys/digits.js.map +0 -1
  107. package/lib/keys/edit.js +0 -14
  108. package/lib/keys/edit.js.map +0 -1
  109. package/lib/keys/exponent.js +0 -30
  110. package/lib/keys/exponent.js.map +0 -1
  111. package/lib/keys/fractions.js +0 -29
  112. package/lib/keys/fractions.js.map +0 -1
  113. package/lib/keys/geometry.js +0 -143
  114. package/lib/keys/geometry.js.map +0 -1
  115. package/lib/keys/grades.js +0 -259
  116. package/lib/keys/grades.js.map +0 -1
  117. package/lib/keys/index.js +0 -35
  118. package/lib/keys/index.js.map +0 -1
  119. package/lib/keys/log.js +0 -27
  120. package/lib/keys/log.js.map +0 -1
  121. package/lib/keys/logic.js +0 -19
  122. package/lib/keys/logic.js.map +0 -1
  123. package/lib/keys/matrices.js +0 -19
  124. package/lib/keys/matrices.js.map +0 -1
  125. package/lib/keys/misc.js +0 -69
  126. package/lib/keys/misc.js.map +0 -1
  127. package/lib/keys/navigation.js +0 -20
  128. package/lib/keys/navigation.js.map +0 -1
  129. package/lib/keys/operators.js +0 -15
  130. package/lib/keys/operators.js.map +0 -1
  131. package/lib/keys/statistics.js +0 -40
  132. package/lib/keys/statistics.js.map +0 -1
  133. package/lib/keys/sub-sup.js +0 -19
  134. package/lib/keys/sub-sup.js.map +0 -1
  135. package/lib/keys/trigonometry.js +0 -45
  136. package/lib/keys/trigonometry.js.map +0 -1
  137. package/lib/keys/utils.js +0 -87
  138. package/lib/keys/utils.js.map +0 -1
  139. package/lib/keys/vars.js +0 -24
  140. package/lib/keys/vars.js.map +0 -1
  141. package/lib/latex-bridge.js +0 -445
  142. package/lib/latex-bridge.js.map +0 -1
  143. package/lib/math-input.js +0 -143
  144. package/lib/math-input.js.map +0 -1
  145. package/lib/mathlive-instance.js +0 -585
  146. package/lib/mathlive-instance.js.map +0 -1
  147. package/lib/mf/common-styles.js +0 -203
  148. package/lib/mf/common-styles.js.map +0 -1
  149. package/lib/mf/index.js +0 -84
  150. package/lib/mf/index.js.map +0 -1
  151. package/lib/mf/input.js +0 -372
  152. package/lib/mf/input.js.map +0 -1
  153. package/lib/mf/static.js +0 -500
  154. package/lib/mf/static.js.map +0 -1
  155. package/lib/selection.js +0 -148
  156. package/lib/selection.js.map +0 -1
  157. package/src/__tests__/composites.test.jsx +0 -220
  158. package/src/__tests__/drop-in-compat.test.jsx +0 -112
  159. package/src/__tests__/index.test.jsx +0 -109
  160. package/src/__tests__/input-imperative.test.jsx +0 -144
  161. package/src/__tests__/input-selection.test.jsx +0 -64
  162. package/src/__tests__/keypad-label.test.jsx +0 -283
  163. package/src/__tests__/keypad-render.test.jsx +0 -123
  164. package/src/__tests__/latex-bridge.test.js +0 -359
  165. package/src/__tests__/mathlive-instance.test.js +0 -418
  166. package/src/__tests__/mount-race.test.jsx +0 -142
  167. package/src/__tests__/selection.test.js +0 -212
  168. package/src/__tests__/static-prompts.test.jsx +0 -364
  169. package/src/horizontal-keypad.jsx +0 -73
  170. package/src/index.jsx +0 -63
  171. package/src/keypad/index.jsx +0 -386
  172. package/src/keypad/keys-layout.js +0 -16
  173. package/src/keys/basic-operators.js +0 -32
  174. package/src/keys/chars.js +0 -5
  175. package/src/keys/comparison.js +0 -28
  176. package/src/keys/constants.js +0 -35
  177. package/src/keys/digits.js +0 -40
  178. package/src/keys/edit.js +0 -3
  179. package/src/keys/exponent.js +0 -28
  180. package/src/keys/fractions.js +0 -26
  181. package/src/keys/geometry.js +0 -149
  182. package/src/keys/grades.js +0 -367
  183. package/src/keys/index.js +0 -20
  184. package/src/keys/log.js +0 -22
  185. package/src/keys/logic.js +0 -15
  186. package/src/keys/matrices.js +0 -15
  187. package/src/keys/misc.js +0 -71
  188. package/src/keys/navigation.js +0 -8
  189. package/src/keys/operators.js +0 -10
  190. package/src/keys/statistics.js +0 -38
  191. package/src/keys/sub-sup.js +0 -15
  192. package/src/keys/trigonometry.js +0 -15
  193. package/src/keys/utils.js +0 -66
  194. package/src/keys/vars.js +0 -19
  195. package/src/latex-bridge.js +0 -449
  196. package/src/math-input.jsx +0 -115
  197. package/src/mathlive-instance.js +0 -582
  198. package/src/mf/common-styles.js +0 -210
  199. package/src/mf/index.js +0 -30
  200. package/src/mf/input.jsx +0 -328
  201. package/src/mf/static.jsx +0 -479
  202. package/src/selection.js +0 -146
@@ -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
- });
@@ -1,64 +0,0 @@
1
- /**
2
- * The editable Input keeps its selection snapped to whole atoms (see
3
- * selection.js). This checks the wiring: attached on mount, removed on unmount.
4
- */
5
- jest.mock('../mathlive-instance', () => {
6
- class FakeMathfieldElement {
7
- constructor() {
8
- const el = global.document.createElement('math-field');
9
-
10
- el.value = '';
11
- el.getValue = () => el.value;
12
-
13
- return el;
14
- }
15
- }
16
-
17
- return {
18
- loadMathLive: () => Promise.resolve({ MathfieldElement: FakeMathfieldElement }),
19
- getMacros: () => ({}),
20
- MATH_MODE_SPACE: '\\ ',
21
- applyShadowStyles: () => {},
22
- trackPlaceholderCaret: () => () => {},
23
- latexToMarkup: (l) => `<span>${l}</span>`,
24
- };
25
- });
26
-
27
- const mockUntrack = jest.fn();
28
-
29
- jest.mock('../selection', () => ({
30
- trackWholeAtomSelection: jest.fn(() => mockUntrack),
31
- }));
32
-
33
- // eslint-disable-next-line import/first
34
- import Input from '../mf/input';
35
- // eslint-disable-next-line import/first
36
- import { trackWholeAtomSelection } from '../selection';
37
-
38
- describe('Input: whole-atom selection', () => {
39
- beforeEach(() => {
40
- trackWholeAtomSelection.mockClear();
41
- mockUntrack.mockClear();
42
- });
43
-
44
- it('attaches the tracker to the mathfield on mount', async () => {
45
- const input = new Input({});
46
-
47
- input.holderRef = { current: document.createElement('span') };
48
- await input.componentDidMount();
49
-
50
- expect(trackWholeAtomSelection).toHaveBeenCalledTimes(1);
51
- expect(trackWholeAtomSelection).toHaveBeenCalledWith(input.mathField);
52
- });
53
-
54
- it('removes it on unmount', async () => {
55
- const input = new Input({});
56
-
57
- input.holderRef = { current: document.createElement('span') };
58
- await input.componentDidMount();
59
- input.componentWillUnmount();
60
-
61
- expect(mockUntrack).toHaveBeenCalledTimes(1);
62
- expect(input.untrackSelection).toBeUndefined();
63
- });
64
- });
@@ -1,283 +0,0 @@
1
- import { withVisibleEmptySlots as latexForDisplay } from '../latex-bridge';
2
- import { keyToAction, toMathLive } from '../latex-bridge';
3
-
4
- /**
5
- * Keys use empty groups to show a command's shape (`\frac{}{}`, `x^{}`).
6
- * MathLive renders `{}` as nothing, so labels need a visible stand-in - but the
7
- * value that gets *inserted* must stay untouched.
8
- */
9
- describe('keypad label display normalisation', () => {
10
- it('gives empty groups a visible glyph', () => {
11
- const out = latexForDisplay('\\frac{}{}');
12
-
13
- expect(out).not.toEqual('\\frac{}{}');
14
- // a \rule, not a glyph: font metrics for a substituted glyph do not match
15
- // MathLive's, which left stretchy accents narrower than the box
16
- expect(out).toContain('\\rule');
17
- expect(out).toContain('pie-empty');
18
- });
19
-
20
- it.each([
21
- ['absolute value', '\\abs{}', '|', '|'],
22
- ['parenthesis', '\\left(\\right)', '(', ')'],
23
- ['brackets', '\\left[\\right]', '[', ']'],
24
- ])('gives an empty %s a tall, filled slot (the same muted tag as other slots)', (_name, latex, open, close) => {
25
- const out = latexForDisplay(toMathLive(latex));
26
-
27
- expect(out).toContain('pie-empty=1');
28
- expect(out).toContain('\\rule[-0.25em]{0.55em}{1em}');
29
- expect(out).not.toContain('\\boxed');
30
- // the strut that keeps the fences tall while the box is centred on the axis
31
- expect(out).toContain('\\rule{0em}{1em}');
32
- expect(out.startsWith(`\\left${open}`)).toBe(true);
33
- expect(out.endsWith(`\\right${close}`)).toBe(true);
34
- });
35
-
36
- it('lifts the bar of an empty segment so its box lines up with the ray and line keys', () => {
37
- const out = latexForDisplay('\\overline{}');
38
-
39
- expect(out).toContain('\\overline{\\rule{0em}{1.08em}');
40
- expect(out).toContain('pie-empty');
41
- // the ray and line keys are left as they were
42
- expect(latexForDisplay('\\overrightarrow{}')).not.toContain('\\rule{0em}');
43
- expect(latexForDisplay('\\overleftrightarrow{}')).not.toContain('\\rule{0em}');
44
- // a filled bar is untouched
45
- expect(latexForDisplay('\\overline{AB}')).toEqual('\\overline{AB}');
46
- });
47
-
48
- it('leaves a filled pair of fences alone', () => {
49
- expect(latexForDisplay(toMathLive('\\abs{x}'))).toEqual('\\left|x\\right|');
50
- expect(latexForDisplay('\\left(x\\right)')).toEqual('\\left(x\\right)');
51
- });
52
-
53
- it('handles every empty-group key shape', () => {
54
- [
55
- '\\frac{}{}',
56
- '\\overline{}',
57
- 'x^{}',
58
- 'x_{}',
59
- '\\sqrt{}',
60
- '\\sqrt[{}]{}',
61
- '\\abs{}',
62
- '\\log_{}',
63
- '\\longdiv{}',
64
- ].forEach((latex) => {
65
- expect(latexForDisplay(latex)).not.toContain('{}');
66
- });
67
- });
68
-
69
- it('treats a whitespace-only group as empty', () => {
70
- // e.g. \frac{x}{ }
71
- expect(latexForDisplay('\\frac{x}{ }')).toContain('pie-empty');
72
- });
73
-
74
- // MathQuill needed a nested \overline{} to draw the empty box. MathLive
75
- // renders that as a real line, so those labels showed a spurious bar (a line
76
- // under the arrows, and a straight line instead of the arc).
77
- it('accent keys do not carry the nested-overline box hack', () => {
78
- // eslint-disable-next-line global-require
79
- const geometry = require('../keys/geometry');
80
-
81
- Object.values(geometry).forEach((key) => {
82
- if (key && typeof key.latex === 'string') {
83
- expect(key.latex).not.toContain('\\overline{}}');
84
- }
85
- });
86
- });
87
-
88
- it('an accent over an empty slot still gets a visible box', () => {
89
- ['\\overleftrightarrow{}', '\\overarc{}'].forEach((latex) => {
90
- const out = latexForDisplay(latex);
91
-
92
- expect(out).toContain('pie-empty');
93
- expect(out).not.toContain('\\overline');
94
- });
95
- });
96
-
97
- // A glyph's width comes from font metrics; U+25AB is not in the KaTeX fonts,
98
- // so the browser substituted a wider one and stretchy accents came out
99
- // narrower than the box. A rule has an explicit width.
100
- it('the empty slot has an explicit width, not font-dependent metrics', () => {
101
- const out = latexForDisplay('\\overarc{}');
102
-
103
- expect(out).toContain('\\rule');
104
- expect(out).toMatch(/rule\{[\d.]+em\}\{[\d.]+em\}/);
105
- expect(out).not.toContain('unicode');
106
- });
107
-
108
- // Measured in the browser: the accent's immediate wrapper computes to 0px
109
- // (its .ML__stretchy child is absolutely positioned), so the svg width has to
110
- // resolve against .ML__base - whose width equals the content. Without pinning
111
- // that, the arc collapsed to a spike and sat right of the box.
112
- it('pins the accent containing block and drops the centering shift', () => {
113
- // eslint-disable-next-line global-require
114
- const { placeholderStyles } = require('../mf/common-styles');
115
-
116
- expect(placeholderStyles['& .ML__base'].position).toEqual('relative');
117
- expect(placeholderStyles['& .ML__stretchy']).toEqual({ position: 'absolute', left: 0, width: '100%' });
118
- // must be !important: MathLive writes margin-left as an inline style, and a
119
- // class cannot override that. Leaving it un-important also halved the arc's
120
- // width, because .ML__center is the stretchy's containing block.
121
- expect(placeholderStyles['& .ML__center:has(.ML__stretchy)'].marginLeft).toEqual('0 !important');
122
- });
123
-
124
- // MathLive sets the index of \sqrt[n]{x} in scriptscriptstyle - an inline
125
- // `font-size: 50%` - which is too small to read on the nth-root key and in
126
- // authored radicals. It is scaled up to 80%.
127
- describe('radical index size', () => {
128
- // Real MathLive 0.110 output for `\sqrt[1]{2}`, trimmed to the index. The
129
- // selector walks this exact chain, so this fixture is what pins it down.
130
- const INDEX_MARKUP = `
131
- <span class="ML__base">
132
- <span class="ML__sqrt-index">
133
- <span class="ML__vlist-t"><span class="ML__vlist-r"><span class="ML__vlist" style="height:0.78em">
134
- <span style="top:-3.45em">
135
- <span class="ML__pstrut" style="height:3em"></span>
136
- <span id="index-size" style="height:0.33em;display:inline-block;font-size: 50%">
137
- <span class="ML__cmr">1</span>
138
- <span id="nested-size" style="font-size: 50%">nested</span>
139
- </span>
140
- </span>
141
- </span></span></span>
142
- </span>
143
- <span class="ML__sqrt-sign"><span class="ML__delim-size1">&#8730;</span></span>
144
- </span>`;
145
-
146
- const rule = () => {
147
- // eslint-disable-next-line global-require
148
- const { rootIndexStyles } = require('../mf/common-styles');
149
- const selectors = Object.keys(rootIndexStyles);
150
-
151
- expect(selectors).toHaveLength(1);
152
-
153
- return { selector: selectors[0], value: rootIndexStyles[selectors[0]].fontSize };
154
- };
155
-
156
- it("scales the index glyph to 80%, up from MathLive's 50%", () => {
157
- // !important: the 50% is an inline style, which a class cannot override.
158
- expect(rule().value).toEqual('80% !important');
159
- });
160
-
161
- it('targets the span MathLive puts the inline font-size on', () => {
162
- const holder = document.createElement('div');
163
-
164
- holder.innerHTML = INDEX_MARKUP;
165
-
166
- const matched = holder.querySelectorAll(rule().selector.replace(/^&\s*/, ''));
167
-
168
- expect(Array.from(matched).map((el) => el.id)).toEqual(['index-size']);
169
- });
170
-
171
- it('leaves the radical index box itself unscaled', () => {
172
- // Scaling `.ML__sqrt-index` would scale the em-based top/height offsets
173
- // that tuck the index into the radical, lifting it clear of the crook.
174
- const { selector } = rule();
175
-
176
- expect(selector).not.toMatch(/\.ML__sqrt-index\s*\{?$/);
177
- expect(selector.startsWith('& .ML__sqrt-index >')).toBe(true);
178
- });
179
-
180
- it('applies wherever math renders - keypad labels, static math, mathfield', () => {
181
- // eslint-disable-next-line global-require
182
- const cs = require('../mf/common-styles');
183
- const { selector } = rule();
184
-
185
- ['commonKeyboardStyles', 'commonMathLiveStyles', 'supsubStyles'].forEach((name) => {
186
- expect(cs[name][selector]).toBeDefined();
187
- });
188
-
189
- // The live mathfield renders in a shadow root, which host styles cannot
190
- // reach, so the same rule ships as plain CSS for `applyShadowStyles`.
191
- expect(cs.ROOT_INDEX_CSS).toContain('.ML__sqrt-index >');
192
- // Derived, so the size lives in one place: the two copies cannot drift.
193
- expect(cs.ROOT_INDEX_CSS).toContain(`font-size:${rule().value}`);
194
- });
195
- });
196
-
197
- it('applies those rules wherever static math renders', () => {
198
- // eslint-disable-next-line global-require
199
- const cs = require('../mf/common-styles');
200
-
201
- ['commonKeyboardStyles', 'commonMathLiveStyles'].forEach((name) => {
202
- expect(cs[name]['& .ML__stretchy']).toBeDefined();
203
- expect(cs[name]['& .ML__base']).toBeDefined();
204
- });
205
- });
206
-
207
- // A bare `& svg` rule re-parents every svg in the subtree; MathLive's own rule
208
- // is descendant-scoped to .ML__latex .ML__stretchy svg.
209
- it('does not use a blanket svg rule', () => {
210
- // eslint-disable-next-line global-require
211
- const cs = require('../mf/common-styles');
212
-
213
- Object.values(cs).forEach((style) => {
214
- if (style && typeof style === 'object') {
215
- expect(Object.keys(style)).not.toContain('& svg');
216
- }
217
- });
218
- });
219
-
220
- // The Percent key showed as a blank button on the Advanced Algebra keypad
221
- // (row 1, position 6). MathQuill defined `LatexCmds['%']` as a symbol that
222
- // serialised to `\\%`, so its bare `%` label parsed fine; MathLive parses real
223
- // LaTeX, where `%` opens a comment - the label rendered nothing, and pressing
224
- // the key inserted a comment that swallowed the rest of the expression.
225
- it('the percent key is escaped, not a latex comment', () => {
226
- // eslint-disable-next-line global-require
227
- const { percentage } = require('../keys/misc');
228
-
229
- expect(percentage.latex).toEqual('\\%');
230
- expect(keyToAction(percentage).value).toEqual('\\%');
231
- });
232
-
233
- // Any bare `%` in a key would fail the same way, silently.
234
- it('no keypad key carries an unescaped %', () => {
235
- // eslint-disable-next-line global-require
236
- const { baseSet } = require('../keys');
237
- // eslint-disable-next-line global-require
238
- const { gradeSets } = require('../keys/grades');
239
-
240
- const every = [...baseSet, ...gradeSets.map((g) => g.set)];
241
-
242
- const walk = (node) => {
243
- if (Array.isArray(node)) {
244
- node.forEach(walk);
245
- return;
246
- }
247
-
248
- if (!node) {
249
- return;
250
- }
251
-
252
- ['latex', 'write', 'command'].forEach((field) => {
253
- const value = node[field];
254
-
255
- if (typeof value === 'string') {
256
- // a `%` is only safe when preceded by an odd run of backslashes
257
- expect(value.replace(/\\./g, '')).not.toContain('%');
258
- }
259
- });
260
- };
261
-
262
- walk(every);
263
- });
264
-
265
- it('leaves latex without empty groups alone', () => {
266
- ['\\pi', '\\sin', 'x^2', '\\overline{AB}'].forEach((latex) => {
267
- expect(latexForDisplay(latex)).toEqual(latex);
268
- });
269
- });
270
-
271
- it('handles nullish input', () => {
272
- expect(latexForDisplay(undefined)).toEqual('');
273
- expect(latexForDisplay('')).toEqual('');
274
- });
275
-
276
- it('does NOT affect what gets inserted into the field', () => {
277
- // the action must use the key's own latex, never the display form
278
- const key = { name: 'frac', latex: '\\frac{}{}' };
279
-
280
- expect(keyToAction(key).value).toEqual('\\frac{}{}');
281
- expect(keyToAction(key).value).not.toContain('pie-empty');
282
- });
283
- });
@@ -1,123 +0,0 @@
1
- import React from 'react';
2
- import { render } from '@testing-library/react';
3
-
4
- import KeyPad, { LatexLabel } from '../keypad';
5
-
6
- /**
7
- * Keypad behaviour that is new in this package: labels are static markup from
8
- * convertLatexToMarkup (the MathQuill version created a live StaticMath per
9
- * button), with a raw-latex fallback when the engine is unavailable.
10
- */
11
- describe('LatexLabel', () => {
12
- it('falls back to raw latex when MathLive is unavailable', () => {
13
- // jsdom cannot load MathLive, so latexToMarkup returns '' - the button must
14
- // still show something rather than render blank.
15
- const { container } = render(<LatexLabel latex={'\\pi'} />);
16
-
17
- expect(container.textContent).toContain('\\pi');
18
- });
19
-
20
- it('re-renders when the latex prop changes', () => {
21
- const { container, rerender } = render(<LatexLabel latex={'\\pi'} />);
22
-
23
- rerender(<LatexLabel latex={'\\theta'} />);
24
-
25
- expect(container.textContent).toContain('\\theta');
26
- });
27
-
28
- it('does not warn more than once about a missing engine', async () => {
29
- LatexLabel.warned = false;
30
-
31
- const a = new LatexLabel({ latex: '\\pi' });
32
- const b = new LatexLabel({ latex: '\\theta' });
33
-
34
- a.state = { markup: '' };
35
- b.state = { markup: '' };
36
- a.setState = () => {};
37
- b.setState = () => {};
38
-
39
- await a.componentDidMount();
40
- await b.componentDidMount();
41
-
42
- expect(LatexLabel.warned).toBe(true);
43
- });
44
- });
45
-
46
- describe('KeyPad', () => {
47
- const key = (over = {}) => ({ name: 'x', label: 'x', write: 'x', category: 'vars', ...over });
48
-
49
- it('renders a button per key', () => {
50
- const { container } = render(<KeyPad onPress={() => {}} baseSet={[[key(), key({ name: 'y', label: 'y' })]]} />);
51
-
52
- expect(container.querySelectorAll('button').length).toBe(2);
53
- });
54
-
55
- it('calls onPress with the whole key definition', () => {
56
- const onPress = jest.fn();
57
- const k = key();
58
- const { container } = render(<KeyPad onPress={onPress} baseSet={[[k]]} />);
59
-
60
- container.querySelector('button').click();
61
-
62
- expect(onPress).toHaveBeenCalledWith(k);
63
- });
64
-
65
- it('renders a latex key through LatexLabel', () => {
66
- const { container } = render(<KeyPad onPress={() => {}} baseSet={[[{ name: 'pi', latex: '\\pi' }]]} />);
67
-
68
- expect(container.textContent).toContain('\\pi');
69
- });
70
-
71
- it('disables the decimal separators when noDecimal is set', () => {
72
- const dot = key({ name: '.', label: '.', write: '.' });
73
- const comma = key({ name: ',', label: ',', write: ',' });
74
- const { container } = render(<KeyPad onPress={() => {}} noDecimal baseSet={[[dot, comma]]} />);
75
-
76
- const disabled = Array.from(container.querySelectorAll('button')).filter((b) => b.disabled);
77
-
78
- expect(disabled.length).toBe(2);
79
- });
80
-
81
- it('leaves the separators enabled otherwise', () => {
82
- const dot = key({ name: '.', label: '.', write: '.' });
83
- const { container } = render(<KeyPad onPress={() => {}} baseSet={[[dot]]} />);
84
-
85
- expect(container.querySelector('button').disabled).toBe(false);
86
- });
87
-
88
- it('renders a placeholder for empty grid slots', () => {
89
- expect(() => render(<KeyPad onPress={() => {}} baseSet={[[null]]} />)).not.toThrow();
90
- });
91
-
92
- it('notifies setKeypadInteraction on pointer interaction', () => {
93
- const setKeypadInteraction = jest.fn();
94
- const { container } = render(
95
- <KeyPad onPress={() => {}} setKeypadInteraction={setKeypadInteraction} baseSet={[[key()]]} />,
96
- );
97
-
98
- container.firstChild.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
99
-
100
- expect(setKeypadInteraction).toHaveBeenCalledWith(true);
101
- });
102
-
103
- it('uses the mode as a class so mode-specific styling applies', () => {
104
- const { container } = render(<KeyPad onPress={() => {}} mode="language" baseSet={[[key()]]} />);
105
-
106
- expect(container.firstChild.className).toContain('language');
107
- });
108
-
109
- it('drops the base set for the modes that supply their own keys', () => {
110
- const { container } = render(
111
- <KeyPad onPress={() => {}} mode="language" baseSet={[[key(), key()]]} additionalKeys={[[key()]]} />,
112
- );
113
-
114
- // base set ignored, only the additional key renders
115
- expect(container.querySelectorAll('button').length).toBe(1);
116
- });
117
-
118
- it('unmounts without throwing', () => {
119
- const { unmount } = render(<KeyPad onPress={() => {}} controlledKeypadMode baseSet={[[key()]]} />);
120
-
121
- expect(() => unmount()).not.toThrow();
122
- });
123
- });