@pie-lib/math-input-mathlive 1.0.0
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 +119 -0
- package/lib/__tests__/index.test.js +118 -0
- package/lib/__tests__/index.test.js.map +1 -0
- package/lib/__tests__/latex-bridge.test.js +186 -0
- package/lib/__tests__/latex-bridge.test.js.map +1 -0
- package/lib/__tests__/mathlive-instance.test.js +137 -0
- package/lib/__tests__/mathlive-instance.test.js.map +1 -0
- package/lib/horizontal-keypad.js +101 -0
- package/lib/horizontal-keypad.js.map +1 -0
- package/lib/index.js +143 -0
- package/lib/index.js.map +1 -0
- package/lib/keypad/index.js +365 -0
- package/lib/keypad/index.js.map +1 -0
- package/lib/keypad/keys-layout.js +22 -0
- package/lib/keypad/keys-layout.js.map +1 -0
- package/lib/keys/basic-operators.js +33 -0
- package/lib/keys/basic-operators.js.map +1 -0
- package/lib/keys/chars.js +12 -0
- package/lib/keys/chars.js.map +1 -0
- package/lib/keys/comparison.js +39 -0
- package/lib/keys/comparison.js.map +1 -0
- package/lib/keys/constants.js +37 -0
- package/lib/keys/constants.js.map +1 -0
- package/lib/keys/digits.js +46 -0
- package/lib/keys/digits.js.map +1 -0
- package/lib/keys/edit.js +14 -0
- package/lib/keys/edit.js.map +1 -0
- package/lib/keys/exponent.js +30 -0
- package/lib/keys/exponent.js.map +1 -0
- package/lib/keys/fractions.js +29 -0
- package/lib/keys/fractions.js.map +1 -0
- package/lib/keys/geometry.js +140 -0
- package/lib/keys/geometry.js.map +1 -0
- package/lib/keys/grades.js +259 -0
- package/lib/keys/grades.js.map +1 -0
- package/lib/keys/index.js +35 -0
- package/lib/keys/index.js.map +1 -0
- package/lib/keys/log.js +27 -0
- package/lib/keys/log.js.map +1 -0
- package/lib/keys/logic.js +19 -0
- package/lib/keys/logic.js.map +1 -0
- package/lib/keys/matrices.js +19 -0
- package/lib/keys/matrices.js.map +1 -0
- package/lib/keys/misc.js +62 -0
- package/lib/keys/misc.js.map +1 -0
- package/lib/keys/navigation.js +20 -0
- package/lib/keys/navigation.js.map +1 -0
- package/lib/keys/operators.js +15 -0
- package/lib/keys/operators.js.map +1 -0
- package/lib/keys/statistics.js +40 -0
- package/lib/keys/statistics.js.map +1 -0
- package/lib/keys/sub-sup.js +19 -0
- package/lib/keys/sub-sup.js.map +1 -0
- package/lib/keys/trigonometry.js +45 -0
- package/lib/keys/trigonometry.js.map +1 -0
- package/lib/keys/utils.js +87 -0
- package/lib/keys/utils.js.map +1 -0
- package/lib/keys/vars.js +24 -0
- package/lib/keys/vars.js.map +1 -0
- package/lib/latex-bridge.js +195 -0
- package/lib/latex-bridge.js.map +1 -0
- package/lib/math-input.js +143 -0
- package/lib/math-input.js.map +1 -0
- package/lib/mathlive-instance.js +319 -0
- package/lib/mathlive-instance.js.map +1 -0
- package/lib/mf/common-styles.js +111 -0
- package/lib/mf/common-styles.js.map +1 -0
- package/lib/mf/index.js +72 -0
- package/lib/mf/index.js.map +1 -0
- package/lib/mf/input.js +266 -0
- package/lib/mf/input.js.map +1 -0
- package/lib/mf/static.js +396 -0
- package/lib/mf/static.js.map +1 -0
- package/package.json +31 -0
- package/src/__tests__/index.test.jsx +109 -0
- package/src/__tests__/latex-bridge.test.js +151 -0
- package/src/__tests__/mathlive-instance.test.js +130 -0
- package/src/horizontal-keypad.jsx +73 -0
- package/src/index.jsx +63 -0
- package/src/keypad/index.jsx +335 -0
- package/src/keypad/keys-layout.js +16 -0
- package/src/keys/basic-operators.js +32 -0
- package/src/keys/chars.js +5 -0
- package/src/keys/comparison.js +28 -0
- package/src/keys/constants.js +35 -0
- package/src/keys/digits.js +40 -0
- package/src/keys/edit.js +3 -0
- package/src/keys/exponent.js +28 -0
- package/src/keys/fractions.js +26 -0
- package/src/keys/geometry.js +144 -0
- package/src/keys/grades.js +367 -0
- package/src/keys/index.js +20 -0
- package/src/keys/log.js +22 -0
- package/src/keys/logic.js +15 -0
- package/src/keys/matrices.js +15 -0
- package/src/keys/misc.js +65 -0
- package/src/keys/navigation.js +8 -0
- package/src/keys/operators.js +10 -0
- package/src/keys/statistics.js +38 -0
- package/src/keys/sub-sup.js +15 -0
- package/src/keys/trigonometry.js +15 -0
- package/src/keys/utils.js +66 -0
- package/src/keys/vars.js +19 -0
- package/src/latex-bridge.js +190 -0
- package/src/math-input.jsx +115 -0
- package/src/mathlive-instance.js +286 -0
- package/src/mf/common-styles.js +108 -0
- package/src/mf/index.js +26 -0
- package/src/mf/input.jsx +223 -0
- package/src/mf/static.jsx +358 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LaTeX translation layer between the MathQuill-era content model and MathLive.
|
|
3
|
+
*
|
|
4
|
+
* Two independent concerns live here:
|
|
5
|
+
*
|
|
6
|
+
* 1. Newlines. MathQuill had no line break, so pie-lib registered a custom
|
|
7
|
+
* `newLine` embed that serialises to `\embed{newLine}[]`. MathLive has no
|
|
8
|
+
* embed system but supports real multiline environments, so we translate
|
|
9
|
+
* `\embed{newLine}[]` <-> `\\` inside `\displaylines{}`.
|
|
10
|
+
*
|
|
11
|
+
* Authored content keeps its `\embed{newLine}[]` on-disk form (that is what
|
|
12
|
+
* @pie-lib/math-rendering already normalises for MathJax), so each renderer
|
|
13
|
+
* normalises at its own boundary and stored items stay compatible.
|
|
14
|
+
*
|
|
15
|
+
* 2. Keypad key -> MathLive action. MathQuill keys carry `write`/`command`/
|
|
16
|
+
* `keystroke` built for `mf.write()` / `mf.cmd()` / `mf.keystroke()`.
|
|
17
|
+
* MathLive has `insert()` and `executeCommand()` instead.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const NEWLINE_EMBED = '\\embed{newLine}[]';
|
|
21
|
+
const NEWLINE_EMBED_REGEX = /\\embed\{newLine\}\[\]/g;
|
|
22
|
+
const DISPLAYLINES_REGEX = /^\\displaylines\{([\s\S]*)\}$/;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* MathQuill's editable sub-field syntax, e.g. `\MathQuillMathField[r1]{}`.
|
|
26
|
+
* These are the answer blocks in templated math items; MathLive expresses the
|
|
27
|
+
* same idea as `\placeholder[id]{}` and reads them back via the prompts API.
|
|
28
|
+
*/
|
|
29
|
+
const MQ_FIELD_REGEX = /\\MathQuillMathField\[([^\]]*)\]\{([^{}]*)\}/g;
|
|
30
|
+
const PLACEHOLDER_REGEX = /\\placeholder\[([^\]]*)\]\{([^{}]*)\}/g;
|
|
31
|
+
|
|
32
|
+
/** Default id used when stored latex has an unnamed field. */
|
|
33
|
+
export const DEFAULT_FIELD_ID = 'r1';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Ids of the answer blocks in a piece of stored latex, in document order.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} latex
|
|
39
|
+
* @returns {string[]}
|
|
40
|
+
*/
|
|
41
|
+
export const fieldIds = (latex) => {
|
|
42
|
+
if (typeof latex !== 'string') {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const out = [];
|
|
47
|
+
let match;
|
|
48
|
+
|
|
49
|
+
MQ_FIELD_REGEX.lastIndex = 0;
|
|
50
|
+
|
|
51
|
+
while ((match = MQ_FIELD_REGEX.exec(latex)) !== null) {
|
|
52
|
+
out.push(match[1] || DEFAULT_FIELD_ID);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return out;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Convert stored latex (which may contain `\embed{newLine}[]`) into latex that
|
|
60
|
+
* MathLive can parse and render as multiple lines.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} latex
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
export const toMathLive = (latex) => {
|
|
66
|
+
if (typeof latex !== 'string' || !latex) {
|
|
67
|
+
return latex || '';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Answer blocks: \MathQuillMathField[id]{x} -> \placeholder[id]{x}
|
|
71
|
+
let out = latex.replace(MQ_FIELD_REGEX, (_m, id, content) => `\\placeholder[${id || DEFAULT_FIELD_ID}]{${content}}`);
|
|
72
|
+
|
|
73
|
+
// Newlines: \embed{newLine}[] -> a multiline environment
|
|
74
|
+
if (out.indexOf(NEWLINE_EMBED) !== -1) {
|
|
75
|
+
out = `\\displaylines{${out.split(NEWLINE_EMBED_REGEX).join(' \\\\ ')}}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return out;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Convert latex out of MathLive back into the stored form, so items authored
|
|
83
|
+
* with the MathLive editor remain readable by @pie-lib/math-rendering and by
|
|
84
|
+
* the existing MathQuill implementation.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} latex
|
|
87
|
+
* @returns {string}
|
|
88
|
+
*/
|
|
89
|
+
export const fromMathLive = (latex) => {
|
|
90
|
+
if (typeof latex !== 'string' || !latex) {
|
|
91
|
+
return latex || '';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const match = latex.trim().match(DISPLAYLINES_REGEX);
|
|
95
|
+
let out = match ? match[1] : latex;
|
|
96
|
+
|
|
97
|
+
// `\\` is the line separator inside a multiline environment.
|
|
98
|
+
if (out.indexOf('\\\\') !== -1) {
|
|
99
|
+
out = out
|
|
100
|
+
.split(/\s*\\\\\s*/)
|
|
101
|
+
.map((s) => s.trim())
|
|
102
|
+
.join(NEWLINE_EMBED);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Answer blocks: \placeholder[id]{x} -> \MathQuillMathField[id]{x}
|
|
106
|
+
out = out.replace(
|
|
107
|
+
PLACEHOLDER_REGEX,
|
|
108
|
+
(_m, id, content) => `\\MathQuillMathField[${id || DEFAULT_FIELD_ID}]{${content}}`,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
return out;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* MathQuill `cmd()` semantics: commands that take arguments should land the
|
|
116
|
+
* cursor inside the first argument. MathLive expresses that with `#?`
|
|
117
|
+
* placeholders in the inserted latex.
|
|
118
|
+
*/
|
|
119
|
+
const COMMANDS_WITH_ARGS = {
|
|
120
|
+
'\\frac': '\\frac{#?}{#?}',
|
|
121
|
+
'/': '\\frac{#?}{#?}',
|
|
122
|
+
'\\sqrt': '\\sqrt{#?}',
|
|
123
|
+
'\\nthroot': '\\sqrt[#?]{#?}',
|
|
124
|
+
'\\overline': '\\overline{#?}',
|
|
125
|
+
'\\overrightarrow': '\\overrightarrow{#?}',
|
|
126
|
+
'\\overleftrightarrow': '\\overleftrightarrow{#?}',
|
|
127
|
+
'\\overarc': '\\overarc{#?}',
|
|
128
|
+
'\\longdiv': '\\longdiv{#?}',
|
|
129
|
+
'^': '^{#?}',
|
|
130
|
+
_: '_{#?}',
|
|
131
|
+
'(': '\\left(#?\\right)',
|
|
132
|
+
'[': '\\left[#?\\right]',
|
|
133
|
+
'|': '\\left|#?\\right|',
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* MathQuill keystroke names -> MathLive command selectors.
|
|
138
|
+
*/
|
|
139
|
+
const KEYSTROKES = {
|
|
140
|
+
Left: 'moveToPreviousChar',
|
|
141
|
+
Right: 'moveToNextChar',
|
|
142
|
+
Backspace: 'deleteBackward',
|
|
143
|
+
Delete: 'deleteForward',
|
|
144
|
+
Up: 'moveUp',
|
|
145
|
+
Down: 'moveDown',
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Translate a keypad key definition into a MathLive action.
|
|
150
|
+
*
|
|
151
|
+
* Mirrors the precedence in @pie-lib/math-input's MathInput.keypadPress:
|
|
152
|
+
* latex (unless a command is present) -> write -> command -> keystroke.
|
|
153
|
+
*
|
|
154
|
+
* @param {object} key
|
|
155
|
+
* @returns {{type: 'insert'|'command', value: string}|undefined}
|
|
156
|
+
*/
|
|
157
|
+
export const keyToAction = (key) => {
|
|
158
|
+
if (!key) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (key.latex && !key.command) {
|
|
163
|
+
return { type: 'insert', value: key.latex };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (key.write) {
|
|
167
|
+
return { type: 'insert', value: key.write };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (key.command) {
|
|
171
|
+
// Some keys carry an array of commands (e.g. Measured Angle is
|
|
172
|
+
// ['m', '\\angle'], log-base-n is ['\\log', '_']). MathQuill's `cmd()`
|
|
173
|
+
// applied them in sequence; MathLive inserts one latex string, so expand
|
|
174
|
+
// each part and concatenate.
|
|
175
|
+
const parts = Array.isArray(key.command) ? key.command : [key.command];
|
|
176
|
+
const value = parts.map((c) => COMMANDS_WITH_ARGS[c] || c).join('');
|
|
177
|
+
|
|
178
|
+
return { type: 'insert', value };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (key.keystroke) {
|
|
182
|
+
const selector = KEYSTROKES[key.keystroke];
|
|
183
|
+
|
|
184
|
+
return selector ? { type: 'command', value: selector } : undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return undefined;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export { COMMANDS_WITH_ARGS, KEYSTROKES };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { styled } from '@mui/material/styles';
|
|
4
|
+
import debug from 'debug';
|
|
5
|
+
|
|
6
|
+
import * as mf from './mf';
|
|
7
|
+
import { baseSet } from './keys';
|
|
8
|
+
import KeyPad from './keypad';
|
|
9
|
+
import { keyToAction } from './latex-bridge';
|
|
10
|
+
|
|
11
|
+
const log = debug('pie-lib:math-input-mathlive');
|
|
12
|
+
|
|
13
|
+
const grey = 'rgba(0, 0, 0, 0.23)';
|
|
14
|
+
|
|
15
|
+
const MathInputContainer = styled('div')(({ theme, focused }) => ({
|
|
16
|
+
borderRadius: '4px',
|
|
17
|
+
border: `solid 1px ${focused ? theme.palette.primary.main : grey}`,
|
|
18
|
+
marginTop: theme.spacing(1),
|
|
19
|
+
marginBottom: theme.spacing(1),
|
|
20
|
+
transition: 'border 200ms linear',
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
const PadContainer = styled('div')({
|
|
24
|
+
width: '100%',
|
|
25
|
+
display: 'flex',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const StyledInput = styled(mf.Input)(({ theme }) => ({
|
|
29
|
+
width: '100%',
|
|
30
|
+
transition: 'border 200ms linear',
|
|
31
|
+
padding: theme.spacing(1),
|
|
32
|
+
'& math-field:focus-within': {
|
|
33
|
+
outline: 'none',
|
|
34
|
+
boxShadow: 'none',
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* MathLive-backed equivalent of @pie-lib/math-input's `MathInput`.
|
|
40
|
+
* Same props (`keyset`, `latex`, `onChange`, `displayMode`).
|
|
41
|
+
*/
|
|
42
|
+
export class MathInput extends React.Component {
|
|
43
|
+
static propTypes = {
|
|
44
|
+
className: PropTypes.string,
|
|
45
|
+
keyset: PropTypes.array,
|
|
46
|
+
displayMode: PropTypes.oneOf(['block', 'block-on-focus']),
|
|
47
|
+
latex: PropTypes.string,
|
|
48
|
+
onChange: PropTypes.func,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
static defaultProps = {
|
|
52
|
+
keyset: [],
|
|
53
|
+
displayMode: 'block',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
constructor(props) {
|
|
57
|
+
super(props);
|
|
58
|
+
this.state = { focused: false };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
keypadPress = (key) => {
|
|
62
|
+
log('[keypadPress] key:', key);
|
|
63
|
+
|
|
64
|
+
if (!this.input) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const action = keyToAction(key);
|
|
69
|
+
|
|
70
|
+
if (!action) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (action.type === 'command') {
|
|
75
|
+
this.input.keystroke(action.value);
|
|
76
|
+
} else {
|
|
77
|
+
this.input.write(action.value);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
inputFocus = () => this.setState({ focused: true });
|
|
82
|
+
|
|
83
|
+
inputBlur = () => this.setState({ focused: false });
|
|
84
|
+
|
|
85
|
+
changeLatex = (l) => {
|
|
86
|
+
const { onChange } = this.props;
|
|
87
|
+
|
|
88
|
+
if (onChange && l !== this.props.latex) {
|
|
89
|
+
log('[changeLatex]', l, this.props.latex);
|
|
90
|
+
onChange(l);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
render() {
|
|
95
|
+
const { className, keyset, latex } = this.props;
|
|
96
|
+
const { focused } = this.state;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<MathInputContainer className={className} focused={focused}>
|
|
100
|
+
<StyledInput
|
|
101
|
+
innerRef={(r) => (this.input = r)}
|
|
102
|
+
onFocus={this.inputFocus}
|
|
103
|
+
onBlur={this.inputBlur}
|
|
104
|
+
latex={latex}
|
|
105
|
+
onChange={this.changeLatex}
|
|
106
|
+
/>
|
|
107
|
+
<PadContainer>
|
|
108
|
+
<KeyPad baseSet={baseSet} additionalKeys={keyset} onPress={this.keypadPress} />
|
|
109
|
+
</PadContainer>
|
|
110
|
+
</MathInputContainer>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default MathInput;
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
import { NEWLINE_EMBED, toMathLive } from './latex-bridge';
|
|
3
|
+
|
|
4
|
+
const log = debug('pie-lib:math-input-mathlive:instance');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* MathLive bootstrap + compatibility shims for @pie-lib/math-input's public API.
|
|
8
|
+
*
|
|
9
|
+
* MathLive is loaded lazily and only in the browser: the editor bundle is
|
|
10
|
+
* ~227KB gzip and registers custom elements, so it must never run during SSR.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
let mathlive;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Where the KaTeX woff2 fonts are served from, as configured by the host app.
|
|
17
|
+
* `undefined` means "not configured"; `null` means "explicitly disabled".
|
|
18
|
+
*/
|
|
19
|
+
let fontsDirectory;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Turn a fonts path into a fully-qualified URL.
|
|
23
|
+
*
|
|
24
|
+
* MathLive's default `fontsDirectory` is the relative `'./fonts/'`, which it
|
|
25
|
+
* resolves against its own script URL. Under a bundler (webpack/Next) that
|
|
26
|
+
* script URL is empty, so `new URL('./fonts/', '')` throws
|
|
27
|
+
* "Failed to construct 'URL': Invalid base URL" from `loadFonts`.
|
|
28
|
+
* Resolving against `document.baseURI` avoids that entirely and honours any
|
|
29
|
+
* <base href> / asset prefix the host app uses.
|
|
30
|
+
*/
|
|
31
|
+
const absoluteFontsDirectory = (dir) => {
|
|
32
|
+
if (!dir) {
|
|
33
|
+
return dir;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Already absolute (http://, https://, //cdn...) - MathLive handles these.
|
|
37
|
+
if (/^(?:[a-z+]+:)?\/\//i.test(dir)) {
|
|
38
|
+
return dir;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
return new URL(dir, document.baseURI).href;
|
|
43
|
+
} catch (e) {
|
|
44
|
+
log('could not resolve fontsDirectory "%s": %s', dir, e && e.message);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Tell the package where the MathLive fonts are served from.
|
|
51
|
+
*
|
|
52
|
+
* Call before `loadMathLive()`. Accepts an absolute URL, a root-relative path
|
|
53
|
+
* (`/mathlive-fonts`) or a relative one; pass `null` to skip font loading and
|
|
54
|
+
* fall back to system fonts.
|
|
55
|
+
*
|
|
56
|
+
* The fonts ship inside the `mathlive` package (`node_modules/mathlive/fonts`)
|
|
57
|
+
* and must be copied into whatever the host app serves statically.
|
|
58
|
+
*
|
|
59
|
+
* @param {string|null} dir
|
|
60
|
+
*/
|
|
61
|
+
export const configureFonts = (dir) => {
|
|
62
|
+
fontsDirectory = dir;
|
|
63
|
+
|
|
64
|
+
if (mathlive) {
|
|
65
|
+
mathlive.MathfieldElement.fontsDirectory = absoluteFontsDirectory(dir);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Custom macros for the pie-specific commands that the MathQuill fork
|
|
71
|
+
* understood but stock LaTeX does not.
|
|
72
|
+
*
|
|
73
|
+
* Kept deliberately in sync with the MathJax macros in
|
|
74
|
+
* @pie-lib/math-rendering (`render-math.js`) so the same latex renders the same
|
|
75
|
+
* way in the editor and in the static renderer.
|
|
76
|
+
*/
|
|
77
|
+
export const PIE_MACROS = {
|
|
78
|
+
parallelogram: '\\htmlData{pie-macro=parallelogram}{\\unicode{"25B1}}',
|
|
79
|
+
overarc: '\\overparen{#1}',
|
|
80
|
+
napprox: '\\not\\approx',
|
|
81
|
+
nsim: '\\not\\sim',
|
|
82
|
+
ncong: '\\not\\cong',
|
|
83
|
+
nparallel: '\\not\\parallel',
|
|
84
|
+
perpendicular: '\\perp',
|
|
85
|
+
divide: '\\div',
|
|
86
|
+
degree: '^{\\circ}',
|
|
87
|
+
square: '\\unicode{"25A1}',
|
|
88
|
+
longdiv: '\\htmlData{pie-macro=longdiv}{#1}',
|
|
89
|
+
abs: '\\left|#1\\right|',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Registry for `registerEmbed`.
|
|
94
|
+
*
|
|
95
|
+
* MathLive has no embed plugin system, so an embed is expressed as a macro that
|
|
96
|
+
* expands to real LaTeX. `\htmlData{embed=<name>}` tags the rendered output so
|
|
97
|
+
* consumers can target it with CSS exactly as they targeted the old
|
|
98
|
+
* `htmlString` markup.
|
|
99
|
+
*
|
|
100
|
+
* Known embeds map to native constructs:
|
|
101
|
+
* - `newLine` -> structural, handled by latex-bridge (`\displaylines` + `\\`)
|
|
102
|
+
* - `answerBlock`-> `\placeholder{}` (interactive, read via the prompts API)
|
|
103
|
+
*
|
|
104
|
+
* @type {Map<string, {factory: Function, latex: string}>}
|
|
105
|
+
*/
|
|
106
|
+
const embeds = new Map();
|
|
107
|
+
|
|
108
|
+
const KNOWN_EMBED_LATEX = {
|
|
109
|
+
// newLine is structural: it is translated by latex-bridge before it ever
|
|
110
|
+
// reaches MathLive, so the macro only needs to be harmless.
|
|
111
|
+
newLine: '',
|
|
112
|
+
answerBlock: '\\placeholder{}',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Load MathLive. Resolves to undefined during SSR.
|
|
117
|
+
* @returns {Promise<object|undefined>}
|
|
118
|
+
*/
|
|
119
|
+
export const loadMathLive = async () => {
|
|
120
|
+
if (typeof window === 'undefined') {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!mathlive) {
|
|
125
|
+
try {
|
|
126
|
+
mathlive = await import('mathlive');
|
|
127
|
+
} catch (e) {
|
|
128
|
+
// MathLive is browser-only and ships an ESM-conditional exports map, so
|
|
129
|
+
// the import can fail in test/SSR-ish runtimes. Fail soft: callers all
|
|
130
|
+
// handle a missing instance (static rendering falls back to raw latex).
|
|
131
|
+
log('MathLive could not be loaded: %s', e && e.message);
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Always replace MathLive's relative `'./fonts/'` default: it throws
|
|
136
|
+
// "Invalid base URL" when bundled. Either point at the host app's copy or
|
|
137
|
+
// disable font loading, both of which are safe.
|
|
138
|
+
if (fontsDirectory === undefined) {
|
|
139
|
+
log(
|
|
140
|
+
'no fontsDirectory configured - MathLive will use fallback fonts. ' +
|
|
141
|
+
'Call configureFonts("/path/to/fonts") with a copy of node_modules/mathlive/fonts to enable them.',
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
mathlive.MathfieldElement.fontsDirectory =
|
|
146
|
+
fontsDirectory === undefined ? null : absoluteFontsDirectory(fontsDirectory);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return mathlive;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Synchronous accessor for an already-loaded MathLive.
|
|
154
|
+
* @returns {object|undefined}
|
|
155
|
+
*/
|
|
156
|
+
export const getMathLive = () => mathlive;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The macro dictionary to hand to a mathfield: pie macros plus anything
|
|
160
|
+
* registered through `registerEmbed`.
|
|
161
|
+
*
|
|
162
|
+
* @returns {object}
|
|
163
|
+
*/
|
|
164
|
+
export const getMacros = () => {
|
|
165
|
+
const out = { ...PIE_MACROS };
|
|
166
|
+
|
|
167
|
+
embeds.forEach(({ latex }, name) => {
|
|
168
|
+
out[name] = `\\htmlData{embed=${name}}{${latex}}`;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return out;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Register an embed. API-compatible with @pie-lib/math-input's `registerEmbed`
|
|
176
|
+
* so existing callers do not change.
|
|
177
|
+
*
|
|
178
|
+
* MathQuill's factory returns `{ htmlString, text, latex }`. Only the
|
|
179
|
+
* declarative parts survive: `latex` round-trips through a macro and the
|
|
180
|
+
* styling hook becomes a `data-embed` attribute. Arbitrary DOM injection via
|
|
181
|
+
* `htmlString` has no MathLive equivalent and is ignored.
|
|
182
|
+
*
|
|
183
|
+
* @param {string} name
|
|
184
|
+
* @param {Function} factory
|
|
185
|
+
*/
|
|
186
|
+
export function registerEmbed(name, factory) {
|
|
187
|
+
if (!name || typeof factory !== 'function') {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let definition = {};
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
definition = factory() || {};
|
|
195
|
+
} catch (e) {
|
|
196
|
+
log('embed factory threw for "%s": %s', name, e.message);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const latex = Object.prototype.hasOwnProperty.call(KNOWN_EMBED_LATEX, name)
|
|
200
|
+
? KNOWN_EMBED_LATEX[name]
|
|
201
|
+
: definition.mathliveLatex || '\\placeholder{}';
|
|
202
|
+
|
|
203
|
+
if (!Object.prototype.hasOwnProperty.call(KNOWN_EMBED_LATEX, name) && !definition.mathliveLatex) {
|
|
204
|
+
// Unknown third-party embed: degrade instead of throwing. `htmlString`
|
|
205
|
+
// cannot be reproduced, so it renders as an editable slot.
|
|
206
|
+
log(
|
|
207
|
+
'embed "%s" is not natively supported by MathLive; rendering as a placeholder. ' +
|
|
208
|
+
'Provide `mathliveLatex` in the factory result to control this.',
|
|
209
|
+
name,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
embeds.set(name, { factory, latex });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Embeds registered so far. Exposed for tests/diagnostics.
|
|
218
|
+
* @returns {string[]}
|
|
219
|
+
*/
|
|
220
|
+
export const registeredEmbeds = () => Array.from(embeds.keys());
|
|
221
|
+
|
|
222
|
+
/** Reset the embed registry. Test helper. */
|
|
223
|
+
export const __resetEmbeds = () => embeds.clear();
|
|
224
|
+
|
|
225
|
+
/** Inject a stub MathLive instance. Test helper. */
|
|
226
|
+
export const __setMathLiveForTest = (stub) => {
|
|
227
|
+
mathlive = stub;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Render static math into an element. API-compatible with
|
|
232
|
+
* @pie-lib/math-input's `applyStaticMath`.
|
|
233
|
+
*
|
|
234
|
+
* @param {HTMLElement} element
|
|
235
|
+
* @param {string} [latex] assigned to the element before rendering
|
|
236
|
+
* @param {object} [options] `{ macros }` overrides
|
|
237
|
+
* @returns {HTMLElement|undefined} the element, for parity with the old return
|
|
238
|
+
*/
|
|
239
|
+
export function applyStaticMath(element, latex, options = {}) {
|
|
240
|
+
if (!element || typeof window === 'undefined') {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const ml = getMathLive();
|
|
245
|
+
|
|
246
|
+
if (!ml) {
|
|
247
|
+
log('applyStaticMath called before MathLive loaded - call loadMathLive() first');
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const source = latex === undefined || latex === null ? element.textContent : latex;
|
|
252
|
+
|
|
253
|
+
element.innerHTML = ml.convertLatexToMarkup(toMathLive(source), {
|
|
254
|
+
macros: { ...getMacros(), ...(options.macros || {}) },
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
return element;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Convert latex to markup using the pie macro set. Used for keypad labels and
|
|
262
|
+
* any read-only rendering that does not need a live mathfield.
|
|
263
|
+
*
|
|
264
|
+
* @param {string} latex
|
|
265
|
+
* @param {object} [options]
|
|
266
|
+
* @returns {string} HTML markup, or '' if MathLive is not loaded
|
|
267
|
+
*/
|
|
268
|
+
export function latexToMarkup(latex, options = {}) {
|
|
269
|
+
const ml = getMathLive();
|
|
270
|
+
|
|
271
|
+
if (!ml || !latex) {
|
|
272
|
+
return '';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
return ml.convertLatexToMarkup(toMathLive(latex), {
|
|
277
|
+
macros: { ...getMacros(), ...(options.macros || {}) },
|
|
278
|
+
...options,
|
|
279
|
+
});
|
|
280
|
+
} catch (e) {
|
|
281
|
+
log('convertLatexToMarkup failed for "%s": %s', latex, e.message);
|
|
282
|
+
return '';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export { NEWLINE_EMBED };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared MathLive styling.
|
|
3
|
+
*
|
|
4
|
+
* Replaces @pie-lib/math-input's `common-mq-styles.js`. That file was built
|
|
5
|
+
* around MathQuill's `.mq-*` class names, none of which exist in MathLive
|
|
6
|
+
* output, so the selectors here target MathLive's own structure instead.
|
|
7
|
+
*
|
|
8
|
+
* The four named groups are kept because consumers destructure them:
|
|
9
|
+
*
|
|
10
|
+
* const { commonMqFontStyles, commonMqKeyboardStyles, longdivStyles, supsubStyles }
|
|
11
|
+
* = mq.CommonMqStyles; // math-toolbar/src/editor-and-pad.jsx, math-preview.jsx
|
|
12
|
+
*
|
|
13
|
+
* Most are intentionally near-empty: MathLive sizes scripts, radicals and
|
|
14
|
+
* fences correctly on its own, so the MathQuill-era corrections are not needed.
|
|
15
|
+
* They stay as documented extension points rather than being deleted, so the
|
|
16
|
+
* destructuring above keeps working.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Font styling. MathQuill needed explicit MathJax font families to match the
|
|
21
|
+
* static renderer; MathLive ships the KaTeX fonts it renders with, so this only
|
|
22
|
+
* smooths rendering and lets the host override the family.
|
|
23
|
+
*/
|
|
24
|
+
export const commonMqFontStyles = {
|
|
25
|
+
'-webkit-font-smoothing': 'antialiased',
|
|
26
|
+
'& math-field': {
|
|
27
|
+
fontFamily: 'inherit',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Long division. The MathQuill fork drew this with nested `.mq-longdiv-*`
|
|
33
|
+
* elements that needed manual offsets; here it comes from the `\longdiv` macro
|
|
34
|
+
* (see `PIE_MACROS`), tagged with `data-pie-macro="longdiv"`.
|
|
35
|
+
*/
|
|
36
|
+
export const longdivStyles = {
|
|
37
|
+
'& [data-pie-macro="longdiv"]': {
|
|
38
|
+
display: 'inline-flex',
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Superscript/subscript sizing. MathLive follows TeX metrics, so no overrides
|
|
44
|
+
* are required.
|
|
45
|
+
*/
|
|
46
|
+
export const supsubStyles = {};
|
|
47
|
+
|
|
48
|
+
/** Empty placeholder boxes (answer blocks / open slots). */
|
|
49
|
+
export const placeholderStyles = {
|
|
50
|
+
'& .ML__placeholder': {
|
|
51
|
+
borderBottom: '1px solid currentColor',
|
|
52
|
+
minWidth: '1em',
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Styling for the on-screen keyboard and any container rendering math labels.
|
|
58
|
+
* Also hides MathLive's own affordances, since pie supplies its own keypad.
|
|
59
|
+
*/
|
|
60
|
+
export const commonMqKeyboardStyles = {
|
|
61
|
+
...commonMqFontStyles,
|
|
62
|
+
...longdivStyles,
|
|
63
|
+
...placeholderStyles,
|
|
64
|
+
touchAction: 'manipulation',
|
|
65
|
+
'& math-field': {
|
|
66
|
+
fontSize: 'inherit',
|
|
67
|
+
outline: 'none',
|
|
68
|
+
},
|
|
69
|
+
'& math-field::part(virtual-keyboard-toggle)': {
|
|
70
|
+
display: 'none',
|
|
71
|
+
},
|
|
72
|
+
'& math-field::part(menu-toggle)': {
|
|
73
|
+
display: 'none',
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Everything combined - the common case for a container of math. */
|
|
78
|
+
export const commonMathLiveStyles = {
|
|
79
|
+
...commonMqFontStyles,
|
|
80
|
+
...longdivStyles,
|
|
81
|
+
...supsubStyles,
|
|
82
|
+
...placeholderStyles,
|
|
83
|
+
'& math-field': {
|
|
84
|
+
fontSize: 'inherit',
|
|
85
|
+
outline: 'none',
|
|
86
|
+
},
|
|
87
|
+
'& math-field::part(virtual-keyboard-toggle)': {
|
|
88
|
+
display: 'none',
|
|
89
|
+
},
|
|
90
|
+
'& math-field::part(menu-toggle)': {
|
|
91
|
+
display: 'none',
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const commonKeyboardStyles = commonMqKeyboardStyles;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Default export mirrors @pie-lib/math-input's `CommonMqStyles` object so
|
|
99
|
+
* existing destructuring keeps working.
|
|
100
|
+
*/
|
|
101
|
+
export default {
|
|
102
|
+
commonMqFontStyles,
|
|
103
|
+
longdivStyles,
|
|
104
|
+
supsubStyles,
|
|
105
|
+
commonMqKeyboardStyles,
|
|
106
|
+
placeholderStyles,
|
|
107
|
+
commonMathLiveStyles,
|
|
108
|
+
};
|