@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.
- package/README.md +84 -109
- package/dist/_virtual/_rolldown/runtime.js +11 -0
- package/dist/horizontal-keypad.d.ts +22 -0
- package/dist/horizontal-keypad.js +54 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +12 -0
- package/dist/keypad/index.d.ts +40 -0
- package/dist/keypad/index.js +245 -0
- package/dist/keypad/keys-layout.d.ts +7 -0
- package/dist/keypad/keys-layout.js +5 -0
- package/dist/keys/basic-operators.d.ts +5 -0
- package/dist/keys/basic-operators.js +30 -0
- package/dist/keys/chars.d.ts +5 -0
- package/dist/keys/comparison.d.ts +4 -0
- package/dist/keys/comparison.js +32 -0
- package/dist/keys/constants.d.ts +4 -0
- package/dist/keys/constants.js +35 -0
- package/dist/keys/digits.d.ts +15 -0
- package/dist/keys/digits.js +34 -0
- package/dist/keys/edit.d.ts +6 -0
- package/dist/keys/edit.js +9 -0
- package/dist/keys/exponent.d.ts +4 -0
- package/dist/keys/exponent.js +28 -0
- package/dist/keys/fractions.d.ts +3 -0
- package/dist/keys/fractions.js +27 -0
- package/dist/keys/geometry.d.ts +23 -0
- package/dist/keys/geometry.js +127 -0
- package/dist/keys/grades.d.ts +9 -0
- package/dist/keys/grades.js +414 -0
- package/dist/keys/index.d.ts +6 -0
- package/dist/keys/index.js +50 -0
- package/dist/keys/log.d.ts +3 -0
- package/dist/keys/log.js +25 -0
- package/dist/keys/logic.d.ts +2 -0
- package/dist/keys/logic.js +13 -0
- package/dist/keys/matrices.d.ts +2 -0
- package/dist/keys/matrices.js +17 -0
- package/dist/keys/misc.d.ts +10 -0
- package/dist/keys/misc.js +60 -0
- package/dist/keys/navigation.d.ts +2 -0
- package/dist/keys/navigation.js +13 -0
- package/dist/keys/operators.d.ts +1 -0
- package/dist/keys/operators.js +11 -0
- package/dist/keys/statistics.d.ts +5 -0
- package/dist/keys/statistics.js +38 -0
- package/dist/keys/sub-sup.d.ts +2 -0
- package/dist/keys/sub-sup.js +17 -0
- package/dist/keys/trigonometry.d.ts +6 -0
- package/dist/keys/trigonometry.js +43 -0
- package/dist/keys/utils.d.ts +5 -0
- package/dist/keys/utils.js +24 -0
- package/dist/keys/vars.d.ts +3 -0
- package/dist/keys/vars.js +22 -0
- package/dist/latex-bridge.d.ts +197 -0
- package/dist/latex-bridge.js +210 -0
- package/dist/longdiv.d.ts +24 -0
- package/dist/longdiv.js +43 -0
- package/dist/math-input.d.ts +26 -0
- package/dist/math-input.js +74 -0
- package/dist/mathlive-instance.d.ts +221 -0
- package/dist/mathlive-instance.js +159 -0
- package/dist/mf/common-styles.d.ts +594 -0
- package/dist/mf/common-styles.js +80 -0
- package/dist/mf/field-label.d.ts +16 -0
- package/dist/mf/field-label.js +13 -0
- package/dist/mf/index.d.ts +4 -0
- package/dist/mf/index.js +21 -0
- package/dist/mf/input.d.ts +94 -0
- package/dist/mf/input.js +127 -0
- package/dist/mf/static.d.ts +143 -0
- package/dist/mf/static.js +230 -0
- package/dist/selection.d.ts +43 -0
- package/dist/selection.js +39 -0
- package/package.json +32 -18
- package/lib/__tests__/composites.test.js +0 -255
- package/lib/__tests__/drop-in-compat.test.js +0 -133
- package/lib/__tests__/embedded-fonts.test.js +0 -121
- package/lib/__tests__/index.test.js +0 -118
- package/lib/__tests__/input-imperative.test.js +0 -155
- package/lib/__tests__/input-selection.test.js +0 -103
- package/lib/__tests__/keypad-label.test.js +0 -231
- package/lib/__tests__/keypad-render.test.js +0 -206
- package/lib/__tests__/latex-bridge.test.js +0 -386
- package/lib/__tests__/mathlive-instance.test.js +0 -419
- package/lib/__tests__/mount-race.test.js +0 -218
- package/lib/__tests__/selection.test.js +0 -195
- package/lib/__tests__/static-prompts.test.js +0 -405
- package/lib/__tests__/tmp-markup.test.js +0 -26
- package/lib/horizontal-keypad.js +0 -101
- package/lib/horizontal-keypad.js.map +0 -1
- package/lib/index.js +0 -143
- package/lib/index.js.map +0 -1
- package/lib/keypad/index.js +0 -431
- package/lib/keypad/index.js.map +0 -1
- package/lib/keypad/keys-layout.js +0 -22
- package/lib/keypad/keys-layout.js.map +0 -1
- package/lib/keys/basic-operators.js +0 -33
- package/lib/keys/basic-operators.js.map +0 -1
- package/lib/keys/chars.js +0 -12
- package/lib/keys/chars.js.map +0 -1
- package/lib/keys/comparison.js +0 -39
- package/lib/keys/comparison.js.map +0 -1
- package/lib/keys/constants.js +0 -37
- package/lib/keys/constants.js.map +0 -1
- package/lib/keys/digits.js +0 -46
- package/lib/keys/digits.js.map +0 -1
- package/lib/keys/edit.js +0 -14
- package/lib/keys/edit.js.map +0 -1
- package/lib/keys/exponent.js +0 -30
- package/lib/keys/exponent.js.map +0 -1
- package/lib/keys/fractions.js +0 -29
- package/lib/keys/fractions.js.map +0 -1
- package/lib/keys/geometry.js +0 -143
- package/lib/keys/geometry.js.map +0 -1
- package/lib/keys/grades.js +0 -259
- package/lib/keys/grades.js.map +0 -1
- package/lib/keys/index.js +0 -35
- package/lib/keys/index.js.map +0 -1
- package/lib/keys/log.js +0 -27
- package/lib/keys/log.js.map +0 -1
- package/lib/keys/logic.js +0 -19
- package/lib/keys/logic.js.map +0 -1
- package/lib/keys/matrices.js +0 -19
- package/lib/keys/matrices.js.map +0 -1
- package/lib/keys/misc.js +0 -69
- package/lib/keys/misc.js.map +0 -1
- package/lib/keys/navigation.js +0 -20
- package/lib/keys/navigation.js.map +0 -1
- package/lib/keys/operators.js +0 -15
- package/lib/keys/operators.js.map +0 -1
- package/lib/keys/statistics.js +0 -40
- package/lib/keys/statistics.js.map +0 -1
- package/lib/keys/sub-sup.js +0 -19
- package/lib/keys/sub-sup.js.map +0 -1
- package/lib/keys/trigonometry.js +0 -45
- package/lib/keys/trigonometry.js.map +0 -1
- package/lib/keys/utils.js +0 -87
- package/lib/keys/utils.js.map +0 -1
- package/lib/keys/vars.js +0 -24
- package/lib/keys/vars.js.map +0 -1
- package/lib/latex-bridge.js +0 -445
- package/lib/latex-bridge.js.map +0 -1
- package/lib/math-input.js +0 -143
- package/lib/math-input.js.map +0 -1
- package/lib/mathlive-instance.js +0 -585
- package/lib/mathlive-instance.js.map +0 -1
- package/lib/mf/common-styles.js +0 -203
- package/lib/mf/common-styles.js.map +0 -1
- package/lib/mf/index.js +0 -84
- package/lib/mf/index.js.map +0 -1
- package/lib/mf/input.js +0 -372
- package/lib/mf/input.js.map +0 -1
- package/lib/mf/static.js +0 -500
- package/lib/mf/static.js.map +0 -1
- package/lib/selection.js +0 -148
- package/lib/selection.js.map +0 -1
- package/src/__tests__/composites.test.jsx +0 -220
- package/src/__tests__/drop-in-compat.test.jsx +0 -112
- package/src/__tests__/index.test.jsx +0 -109
- package/src/__tests__/input-imperative.test.jsx +0 -144
- package/src/__tests__/input-selection.test.jsx +0 -64
- package/src/__tests__/keypad-label.test.jsx +0 -283
- package/src/__tests__/keypad-render.test.jsx +0 -123
- package/src/__tests__/latex-bridge.test.js +0 -359
- package/src/__tests__/mathlive-instance.test.js +0 -418
- package/src/__tests__/mount-race.test.jsx +0 -142
- package/src/__tests__/selection.test.js +0 -212
- package/src/__tests__/static-prompts.test.jsx +0 -364
- package/src/horizontal-keypad.jsx +0 -73
- package/src/index.jsx +0 -63
- package/src/keypad/index.jsx +0 -386
- package/src/keypad/keys-layout.js +0 -16
- package/src/keys/basic-operators.js +0 -32
- package/src/keys/chars.js +0 -5
- package/src/keys/comparison.js +0 -28
- package/src/keys/constants.js +0 -35
- package/src/keys/digits.js +0 -40
- package/src/keys/edit.js +0 -3
- package/src/keys/exponent.js +0 -28
- package/src/keys/fractions.js +0 -26
- package/src/keys/geometry.js +0 -149
- package/src/keys/grades.js +0 -367
- package/src/keys/index.js +0 -20
- package/src/keys/log.js +0 -22
- package/src/keys/logic.js +0 -15
- package/src/keys/matrices.js +0 -15
- package/src/keys/misc.js +0 -71
- package/src/keys/navigation.js +0 -8
- package/src/keys/operators.js +0 -10
- package/src/keys/statistics.js +0 -38
- package/src/keys/sub-sup.js +0 -15
- package/src/keys/trigonometry.js +0 -15
- package/src/keys/utils.js +0 -66
- package/src/keys/vars.js +0 -19
- package/src/latex-bridge.js +0 -449
- package/src/math-input.jsx +0 -115
- package/src/mathlive-instance.js +0 -582
- package/src/mf/common-styles.js +0 -210
- package/src/mf/index.js +0 -30
- package/src/mf/input.jsx +0 -328
- package/src/mf/static.jsx +0 -479
- 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">√</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
|
-
});
|