@pie-lib/math-input-mathlive 1.1.4 → 1.1.5-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -5
- package/lib/keypad/index.js +15 -17
- package/lib/keypad/index.js.map +1 -1
- package/lib/keys/geometry.js +7 -4
- package/lib/keys/geometry.js.map +1 -1
- package/lib/keys/misc.js +9 -2
- package/lib/keys/misc.js.map +1 -1
- package/lib/latex-bridge.js +48 -3
- package/lib/latex-bridge.js.map +1 -1
- package/lib/mathlive-instance.js +173 -12
- package/lib/mathlive-instance.js.map +1 -1
- package/lib/mf/common-styles.js +74 -10
- package/lib/mf/common-styles.js.map +1 -1
- package/lib/mf/index.js +6 -0
- package/lib/mf/index.js.map +1 -1
- package/lib/mf/input.js +29 -5
- package/lib/mf/input.js.map +1 -1
- package/lib/mf/static.js +105 -7
- package/lib/mf/static.js.map +1 -1
- package/package.json +3 -4
- package/src/keypad/index.jsx +15 -14
- package/src/keys/geometry.js +7 -2
- package/src/keys/misc.js +8 -2
- package/src/latex-bridge.js +52 -3
- package/src/mathlive-instance.js +190 -11
- package/src/mf/common-styles.js +78 -8
- package/src/mf/index.js +2 -0
- package/src/mf/input.jsx +37 -5
- package/src/mf/static.jsx +121 -6
- package/CHANGELOG.md +0 -30
- package/LICENSE.md +0 -5
- 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__/keypad-label.test.jsx +0 -57
- package/src/__tests__/latex-bridge.test.js +0 -230
- package/src/__tests__/mathlive-instance.test.js +0 -181
package/src/latex-bridge.js
CHANGED
|
@@ -55,11 +55,19 @@ export const DEFAULT_FIELD_ID = 'r1';
|
|
|
55
55
|
*
|
|
56
56
|
* `\placeholder{}` is not usable here: in static markup it renders as a
|
|
57
57
|
* non-breaking space, because its editable box only exists inside a live
|
|
58
|
-
* mathfield. So
|
|
58
|
+
* mathfield. So something with real width is required.
|
|
59
|
+
*
|
|
60
|
+
* It must be a `\rule`, not a unicode glyph. A glyph's width comes from font
|
|
61
|
+
* metrics, and U+25AB is not in the KaTeX fonts - the browser substitutes a
|
|
62
|
+
* wider fallback while MathLive still sizes surrounding accents from its own
|
|
63
|
+
* metrics. That left stretchy accents (`\overarc`, `\overleftrightarrow`)
|
|
64
|
+
* visibly narrower than the box they were meant to span. A rule carries an
|
|
65
|
+
* explicit em width, so accents size correctly - and a filled box is closer to
|
|
66
|
+
* MathQuill's `.mq-empty` anyway.
|
|
59
67
|
*
|
|
60
68
|
* Live mathfields need none of this - MathLive manages empty slots itself.
|
|
61
69
|
*/
|
|
62
|
-
const EMPTY_SLOT = '\\htmlData{pie-empty=1}{\\
|
|
70
|
+
const EMPTY_SLOT = '\\htmlData{pie-empty=1}{\\rule{0.55em}{0.55em}}';
|
|
63
71
|
const EMPTY_GROUP_REGEX = /\{\s*\}/g;
|
|
64
72
|
|
|
65
73
|
/**
|
|
@@ -106,6 +114,40 @@ export const toNativeCommands = (latex) => {
|
|
|
106
114
|
out = replaceAbs(out);
|
|
107
115
|
out = out.replace(/\\longdiv\{/g, '\\enclose{longdiv}{');
|
|
108
116
|
out = out.replace(/\\overarc\{/g, '\\overparen{');
|
|
117
|
+
out = escapeBareComments(out);
|
|
118
|
+
|
|
119
|
+
return out;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Escape an unescaped `%`.
|
|
124
|
+
*
|
|
125
|
+
* In real LaTeX `%` starts a comment, so MathLive discards it *and everything
|
|
126
|
+
* after it*: `50%` renders as `50`, and `x%y` as just `x`. MathQuill had no
|
|
127
|
+
* such notion - it treated `%` as a symbol - so latex from anywhere outside the
|
|
128
|
+
* keypad may still carry a bare one.
|
|
129
|
+
*
|
|
130
|
+
* Only a `%` preceded by an even number of backslashes is bare: `\%` is already
|
|
131
|
+
* escaped, while the `%` in `\\%` (a line break, then a comment) is not.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} latex
|
|
134
|
+
* @returns {string}
|
|
135
|
+
*/
|
|
136
|
+
const escapeBareComments = (latex) => {
|
|
137
|
+
if (latex.indexOf('%') === -1) {
|
|
138
|
+
return latex;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let out = '';
|
|
142
|
+
// Consecutive backslashes immediately before the current character.
|
|
143
|
+
let backslashes = 0;
|
|
144
|
+
|
|
145
|
+
for (let i = 0; i < latex.length; i++) {
|
|
146
|
+
const ch = latex[i];
|
|
147
|
+
|
|
148
|
+
out += ch === '%' && backslashes % 2 === 0 ? '\\%' : ch;
|
|
149
|
+
backslashes = ch === '\\' ? backslashes + 1 : 0;
|
|
150
|
+
}
|
|
109
151
|
|
|
110
152
|
return out;
|
|
111
153
|
};
|
|
@@ -240,8 +282,15 @@ export const fromMathLive = (latex) => {
|
|
|
240
282
|
* placeholders in the inserted latex.
|
|
241
283
|
*/
|
|
242
284
|
const COMMANDS_WITH_ARGS = {
|
|
285
|
+
// An explicit "blank over blank" key: both slots start empty.
|
|
243
286
|
'\\frac': '\\frac{#?}{#?}',
|
|
244
|
-
'
|
|
287
|
+
// Division, which must behave like MathQuill's `/`: whatever precedes the
|
|
288
|
+
// cursor becomes the numerator and the caret lands in the denominator. `#@`
|
|
289
|
+
// is MathLive's token for "the selection, or the item before the cursor" -
|
|
290
|
+
// this is exactly the template MathLive binds to the `/` key itself. Using
|
|
291
|
+
// `#?` here instead produced an empty fraction and left the typed number
|
|
292
|
+
// stranded outside it.
|
|
293
|
+
'/': '\\frac{#@}{#?}',
|
|
245
294
|
'\\sqrt': '\\sqrt{#?}',
|
|
246
295
|
'\\nthroot': '\\sqrt[#?]{#?}',
|
|
247
296
|
'\\overline': '\\overline{#?}',
|
package/src/mathlive-instance.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import debug from 'debug';
|
|
2
2
|
import { NEWLINE_EMBED, toMathLive, withVisibleEmptySlots } from './latex-bridge';
|
|
3
|
+
import { ROOT_INDEX_CSS } from './mf/common-styles';
|
|
3
4
|
|
|
4
5
|
const log = debug('pie-lib:math-input-mathlive:instance');
|
|
5
6
|
|
|
@@ -46,26 +47,63 @@ const absoluteFontsDirectory = (dir) => {
|
|
|
46
47
|
}
|
|
47
48
|
};
|
|
48
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Version-pinned CDN fallback for the KaTeX fonts.
|
|
52
|
+
*
|
|
53
|
+
* Some symbols are composed from Private Use Area glyphs that exist only in
|
|
54
|
+
* those fonts: `\\neq` is U+E020 (a negation slash) overlapped on `=`, and
|
|
55
|
+
* `\\nsim` / `\\ncong` work the same way. With no fonts the browser substitutes
|
|
56
|
+
* an arbitrary glyph for U+E020, so those keys render as garbage rather than
|
|
57
|
+
* simply falling back to a plainer face.
|
|
58
|
+
*
|
|
59
|
+
* The version comes from MathLive itself, so the fonts always match the engine.
|
|
60
|
+
* Hosts that would rather not depend on a third party should serve the fonts and
|
|
61
|
+
* call `configureFonts('/their/path')`, or pass `null` to opt out.
|
|
62
|
+
*/
|
|
63
|
+
const FALLBACK_MATHLIVE_VERSION = '0.110.0';
|
|
64
|
+
|
|
65
|
+
const cdnFontsDirectory = () => {
|
|
66
|
+
const version = (mathlive && mathlive.version && mathlive.version.mathlive) || FALLBACK_MATHLIVE_VERSION;
|
|
67
|
+
|
|
68
|
+
// NOTE: `/fonts`, not `/dist/fonts` - the published package root holds them.
|
|
69
|
+
return `https://unpkg.com/mathlive@${version}/fonts`;
|
|
70
|
+
};
|
|
71
|
+
|
|
49
72
|
/**
|
|
50
73
|
* Tell the package where the MathLive fonts are served from.
|
|
51
74
|
*
|
|
52
|
-
* Call before `loadMathLive()`.
|
|
53
|
-
*
|
|
54
|
-
*
|
|
75
|
+
* Call before `loadMathLive()`.
|
|
76
|
+
*
|
|
77
|
+
* - a path/URL: served by you, no third-party request
|
|
78
|
+
* - `null`: skip font loading; note `\\neq`, `\\nsim` and `\\ncong` then
|
|
79
|
+
* render incorrectly, being built from Private Use Area glyphs
|
|
80
|
+
* - not called: falls back to a version-pinned CDN
|
|
55
81
|
*
|
|
56
82
|
* The fonts ship inside the `mathlive` package (`node_modules/mathlive/fonts`)
|
|
57
|
-
*
|
|
83
|
+
* ready to copy into whatever the host app serves statically.
|
|
58
84
|
*
|
|
59
|
-
* @param {string|null} dir
|
|
85
|
+
* @param {string|null} [dir]
|
|
60
86
|
*/
|
|
61
87
|
export const configureFonts = (dir) => {
|
|
62
88
|
fontsDirectory = dir;
|
|
63
89
|
|
|
64
90
|
if (mathlive) {
|
|
65
|
-
mathlive.MathfieldElement.fontsDirectory = absoluteFontsDirectory(dir);
|
|
91
|
+
mathlive.MathfieldElement.fontsDirectory = dir === undefined ? cdnFontsDirectory() : absoluteFontsDirectory(dir);
|
|
66
92
|
}
|
|
67
93
|
};
|
|
68
94
|
|
|
95
|
+
/**
|
|
96
|
+
* What the spacebar inserts in math mode.
|
|
97
|
+
*
|
|
98
|
+
* MathLive's `mathModeSpace` defaults to an empty string, which makes the
|
|
99
|
+
* spacebar a no-op - users cannot type a space at all. `\\ ` (a LaTeX control
|
|
100
|
+
* space) is what MathQuill serialised spaces as, so existing content and the
|
|
101
|
+
* MathJax renderer in @pie-lib/math-rendering both already understand it.
|
|
102
|
+
*
|
|
103
|
+
* A literal " " is not usable: LaTeX skips raw whitespace.
|
|
104
|
+
*/
|
|
105
|
+
export const MATH_MODE_SPACE = '\\ ';
|
|
106
|
+
|
|
69
107
|
/**
|
|
70
108
|
* Custom macros for the pie-specific commands that the MathQuill fork
|
|
71
109
|
* understood but stock LaTeX does not.
|
|
@@ -126,21 +164,162 @@ let loadWarned = false;
|
|
|
126
164
|
/** Apply post-load configuration exactly once. */
|
|
127
165
|
const configureLoaded = () => {
|
|
128
166
|
// Always replace MathLive's relative `'./fonts/'` default: it throws
|
|
129
|
-
// "Invalid base URL" when bundled.
|
|
130
|
-
//
|
|
167
|
+
// "Invalid base URL" when bundled. Use the host's copy if one was configured,
|
|
168
|
+
// otherwise the version-pinned CDN.
|
|
131
169
|
if (fontsDirectory === undefined) {
|
|
132
170
|
log(
|
|
133
|
-
'no fontsDirectory configured -
|
|
134
|
-
'
|
|
171
|
+
'no fontsDirectory configured - falling back to the CDN (%s). ' +
|
|
172
|
+
'Serve the fonts yourself and call configureFonts("/path/to/fonts") to avoid the network request, ' +
|
|
173
|
+
'or configureFonts(null) to opt out.',
|
|
174
|
+
cdnFontsDirectory(),
|
|
135
175
|
);
|
|
136
176
|
}
|
|
137
177
|
|
|
138
178
|
mathlive.MathfieldElement.fontsDirectory =
|
|
139
|
-
fontsDirectory === undefined ?
|
|
179
|
+
fontsDirectory === undefined ? cdnFontsDirectory() : absoluteFontsDirectory(fontsDirectory);
|
|
140
180
|
|
|
141
181
|
injectCoreStylesheet();
|
|
142
182
|
};
|
|
143
183
|
|
|
184
|
+
/**
|
|
185
|
+
* CSS that has to live *inside* a mathfield's shadow root.
|
|
186
|
+
*
|
|
187
|
+
* A live `<math-field>` renders in shadow DOM (`attachShadow({mode:'open'})`),
|
|
188
|
+
* so nothing in the host page's stylesheets reaches it. The stretchy-accent
|
|
189
|
+
* problem therefore needs fixing twice: once for static markup (see
|
|
190
|
+
* `placeholderStyles`) and once here for the editor.
|
|
191
|
+
*
|
|
192
|
+
* MathLive centres an accent with `margin-left: <half the content width>` as an
|
|
193
|
+
* inline style. For a stretchy accent that also shrinks its containing block
|
|
194
|
+
* (`.ML__center` is position:relative), so the arc ends up half-width and offset
|
|
195
|
+
* - visibly failing to span the text underneath.
|
|
196
|
+
*
|
|
197
|
+
* `:has(.ML__stretchy)` keeps the fix to stretchy accents; single-glyph accents
|
|
198
|
+
* (`\hat{x}`, `\vec{x}`) still need their centring offset.
|
|
199
|
+
*/
|
|
200
|
+
/**
|
|
201
|
+
* Marker put on the host `<math-field>` while a `\placeholder{}` is selected,
|
|
202
|
+
* so the caret rules below apply to a placeholder and nothing else.
|
|
203
|
+
*/
|
|
204
|
+
export const PLACEHOLDER_CARET_CLASS = 'pie-caret-in-placeholder';
|
|
205
|
+
|
|
206
|
+
/** What MathLive serialises a `#?` slot as. */
|
|
207
|
+
const PLACEHOLDER_LATEX = '\\placeholder{}';
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* A blinking caret inside a selected placeholder box.
|
|
211
|
+
*
|
|
212
|
+
* A keypad key such as `\frac` inserts `\frac{#?}{#?}`. MathLive turns each
|
|
213
|
+
* `#?` into a `\placeholder{}` atom and *selects* the first one. That atom is a
|
|
214
|
+
* leaf - the offsets around it are "before" and "after", there is no position
|
|
215
|
+
* inside it - so MathLive renders **no caret element at all** and shows only
|
|
216
|
+
* the selection highlight. The box therefore looked focused (blue) but dead,
|
|
217
|
+
* where MathQuill used to blink a cursor inside its empty box.
|
|
218
|
+
*
|
|
219
|
+
* So the caret is drawn here. `:not(:has(.ML__selected))` picks the innermost
|
|
220
|
+
* selected element (MathLive puts the class on a wrapper *and* the glyph span,
|
|
221
|
+
* which would otherwise give two carets), and the `ML__caret-blink` keyframes
|
|
222
|
+
* plus `--_caret-color` are MathLive's own, already in this shadow root.
|
|
223
|
+
*
|
|
224
|
+
* Typing still replaces the whole placeholder, which for an empty box is
|
|
225
|
+
* indistinguishable from inserting at the caret.
|
|
226
|
+
*/
|
|
227
|
+
const PLACEHOLDER_CARET_CSS = [
|
|
228
|
+
`:host(.${PLACEHOLDER_CARET_CLASS}) .ML__selected:not(:has(.ML__selected)){position:relative}`,
|
|
229
|
+
`:host(.${PLACEHOLDER_CARET_CLASS}) .ML__selected:not(:has(.ML__selected))::after{`,
|
|
230
|
+
'content:"";position:absolute;left:50%;top:12%;height:76%;',
|
|
231
|
+
'border-right:2px solid var(--_caret-color, currentColor);',
|
|
232
|
+
'animation:ML__caret-blink 1.05s step-end forwards infinite}',
|
|
233
|
+
].join('');
|
|
234
|
+
|
|
235
|
+
// A live mathfield needs the readable radical index too: `rootIndexStyles` is
|
|
236
|
+
// written for static markup in the host page, which cannot cross the shadow
|
|
237
|
+
// boundary.
|
|
238
|
+
const SHADOW_CSS =
|
|
239
|
+
'.ML__latex .ML__center:has(.ML__stretchy){margin-left:0 !important}' + ROOT_INDEX_CSS + PLACEHOLDER_CARET_CSS;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Keep {@link PLACEHOLDER_CARET_CLASS} in step with the selection.
|
|
243
|
+
*
|
|
244
|
+
* Gating on the class is what keeps the caret specific to placeholders: an
|
|
245
|
+
* ordinary selection (`x+1`) must not sprout one. MathLive fires
|
|
246
|
+
* `selection-change` for every caret move, including the one that lands on the
|
|
247
|
+
* placeholder straight after an insert.
|
|
248
|
+
*
|
|
249
|
+
* @param {HTMLElement} mathField
|
|
250
|
+
* @returns {function|undefined} teardown, to call on unmount
|
|
251
|
+
*/
|
|
252
|
+
export const trackPlaceholderCaret = (mathField) => {
|
|
253
|
+
if (!mathField || typeof mathField.addEventListener !== 'function') {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const update = () => {
|
|
258
|
+
let selected = '';
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
// A collapsed selection already has a real MathLive caret.
|
|
262
|
+
if (!mathField.selectionIsCollapsed && mathField.selection) {
|
|
263
|
+
selected = mathField.getValue(mathField.selection, 'latex');
|
|
264
|
+
}
|
|
265
|
+
} catch (e) {
|
|
266
|
+
log('could not read the selection: %s', e && e.message);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (mathField.classList) {
|
|
270
|
+
mathField.classList.toggle(PLACEHOLDER_CARET_CLASS, selected === PLACEHOLDER_LATEX);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
mathField.addEventListener('selection-change', update);
|
|
275
|
+
update();
|
|
276
|
+
|
|
277
|
+
return () => mathField.removeEventListener('selection-change', update);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Apply {@link SHADOW_CSS} inside a mathfield's shadow root.
|
|
282
|
+
*
|
|
283
|
+
* Uses `adoptedStyleSheets` when available (which is how MathLive loads its own
|
|
284
|
+
* stylesheets) and falls back to appending a <style> node.
|
|
285
|
+
*
|
|
286
|
+
* @param {HTMLElement} mathField
|
|
287
|
+
*/
|
|
288
|
+
export const applyShadowStyles = (mathField) => {
|
|
289
|
+
const root = mathField && mathField.shadowRoot;
|
|
290
|
+
|
|
291
|
+
if (!root) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
if ('adoptedStyleSheets' in root && typeof CSSStyleSheet !== 'undefined') {
|
|
297
|
+
// Do not re-add on a re-render.
|
|
298
|
+
if (root.__pieAccentSheet) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const sheet = new CSSStyleSheet();
|
|
303
|
+
|
|
304
|
+
sheet.replaceSync(SHADOW_CSS);
|
|
305
|
+
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
|
|
306
|
+
root.__pieAccentSheet = sheet;
|
|
307
|
+
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!root.querySelector('style[data-pie-accent]')) {
|
|
312
|
+
const style = document.createElement('style');
|
|
313
|
+
|
|
314
|
+
style.setAttribute('data-pie-accent', '');
|
|
315
|
+
style.textContent = SHADOW_CSS;
|
|
316
|
+
root.appendChild(style);
|
|
317
|
+
}
|
|
318
|
+
} catch (e) {
|
|
319
|
+
log('could not apply shadow styles to the mathfield: %s', e && e.message);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
144
323
|
/**
|
|
145
324
|
* Ensure MathLive's "core" stylesheet is in the document.
|
|
146
325
|
*
|
package/src/mf/common-styles.js
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
* Most are intentionally near-empty: MathLive sizes scripts, radicals and
|
|
14
14
|
* fences correctly on its own, so the MathQuill-era corrections are not needed.
|
|
15
15
|
* They stay as documented extension points rather than being deleted, so the
|
|
16
|
-
* destructuring above keeps working.
|
|
16
|
+
* destructuring above keeps working. The exception is the radical index, which
|
|
17
|
+
* MathLive sizes to TeX metrics that read too small here - see
|
|
18
|
+
* `rootIndexStyles`.
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -40,10 +42,47 @@ export const longdivStyles = {
|
|
|
40
42
|
};
|
|
41
43
|
|
|
42
44
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
+
* How the index of a radical is sized.
|
|
46
|
+
*
|
|
47
|
+
* MathLive follows TeX and sets the index of `\sqrt[n]{x}` in scriptscriptstyle,
|
|
48
|
+
* which it emits as an inline `font-size: 50%` on the span wrapping the index's
|
|
49
|
+
* glyphs. At authoring sizes that leaves the index - on the nth-root key, and in
|
|
50
|
+
* any radical the author types - too small to read next to the radicand, so it is
|
|
51
|
+
* scaled up to 80% - 1.6x what MathLive asks for.
|
|
52
|
+
*
|
|
53
|
+
* Only that innermost span is resized. `.ML__sqrt-index` itself keeps its font
|
|
54
|
+
* size on purpose: the vlist inside it tucks the index into the radical's crook
|
|
55
|
+
* with em-based `top`/`height` offsets, and scaling those lifts the index clear
|
|
56
|
+
* of the radical instead of just enlarging it.
|
|
57
|
+
*
|
|
58
|
+
* The child chain is spelled out so that nothing nested deeper inside the index
|
|
59
|
+
* (a fraction's numerator, another radical) is caught by the same rule, and
|
|
60
|
+
* `!important` is required because the 50% is an INLINE style.
|
|
45
61
|
*/
|
|
46
|
-
|
|
62
|
+
const ROOT_INDEX_SELECTOR = '.ML__sqrt-index > .ML__vlist-t > .ML__vlist-r > .ML__vlist > span > [style*="font-size"]';
|
|
63
|
+
const ROOT_INDEX_FONT_SIZE = '80%';
|
|
64
|
+
|
|
65
|
+
export const rootIndexStyles = {
|
|
66
|
+
[`& ${ROOT_INDEX_SELECTOR}`]: {
|
|
67
|
+
fontSize: `${ROOT_INDEX_FONT_SIZE} !important`,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The same rule as a CSS string, for a live mathfield's shadow root - see
|
|
73
|
+
* `SHADOW_CSS` in mathlive-instance.js. Static markup is styled by
|
|
74
|
+
* {@link rootIndexStyles}.
|
|
75
|
+
*/
|
|
76
|
+
export const ROOT_INDEX_CSS = `${ROOT_INDEX_SELECTOR}{font-size:${ROOT_INDEX_FONT_SIZE} !important}`;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Superscript/subscript sizing. MathLive follows TeX metrics for scripts, so the
|
|
80
|
+
* MathQuill-era corrections are gone; the radical index is the one place where a
|
|
81
|
+
* TeX-correct size reads too small in the authoring UI.
|
|
82
|
+
*/
|
|
83
|
+
export const supsubStyles = {
|
|
84
|
+
...rootIndexStyles,
|
|
85
|
+
};
|
|
47
86
|
|
|
48
87
|
/** Empty placeholder boxes (answer blocks / open slots). */
|
|
49
88
|
export const placeholderStyles = {
|
|
@@ -57,13 +96,42 @@ export const placeholderStyles = {
|
|
|
57
96
|
'& [data-pie-empty]': {
|
|
58
97
|
opacity: 0.5,
|
|
59
98
|
},
|
|
60
|
-
// Stretchy accents
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
99
|
+
// Stretchy accents (\overarc, \overleftrightarrow, \overrightarrow) render an
|
|
100
|
+
// <svg width="100%" preserveAspectRatio="none"> inside a `.ML__stretchy`. That
|
|
101
|
+
// span is absolutely positioned, so its immediate wrapper computes to 0px and
|
|
102
|
+
// the width has to resolve against a *positioned* ancestor. Pin that to
|
|
103
|
+
// `.ML__base`, whose width already equals the accent's content (measured:
|
|
104
|
+
// 9.85px for a 0.56em slot), so the arc spans exactly its content.
|
|
105
|
+
//
|
|
106
|
+
// Do NOT write a bare `& svg` rule instead: MathLive's own rule is
|
|
107
|
+
// descendant-scoped (`.ML__latex .ML__stretchy svg`), and a global one
|
|
108
|
+
// re-parents every svg in the subtree.
|
|
109
|
+
'& .ML__base': {
|
|
110
|
+
position: 'relative',
|
|
111
|
+
},
|
|
112
|
+
'& .ML__stretchy': {
|
|
64
113
|
position: 'absolute',
|
|
114
|
+
left: 0,
|
|
65
115
|
width: '100%',
|
|
66
116
|
},
|
|
117
|
+
// MathLive centres an accent by shifting it right half the content width -
|
|
118
|
+
// `margin-left: 0.28em` for a 0.56em slot - which assumes a glyph with its own
|
|
119
|
+
// side bearings. Over a synthetic empty slot it just pushes the arc right.
|
|
120
|
+
//
|
|
121
|
+
// `!important` is required: MathLive writes this as an INLINE style
|
|
122
|
+
// (`style="top:-3.4em;margin-left:0.28em"`), which a class cannot override.
|
|
123
|
+
//
|
|
124
|
+
// It also fixes the width. `.ML__center` is the stretchy's containing block
|
|
125
|
+
// (MathLive gives `.ML__vlist > span` position:relative), and its width is the
|
|
126
|
+
// base minus that margin - measured 9.85px - 4.93px = 4.93px, exactly half,
|
|
127
|
+
// which is why the arc came out half-width AND offset right.
|
|
128
|
+
//
|
|
129
|
+
// Scoped with `:has(.ML__stretchy)`: only stretchy accents are affected.
|
|
130
|
+
// A single-glyph accent (`\hat{x}`, `\vec{x}`) genuinely needs its centring
|
|
131
|
+
// offset, and must keep it.
|
|
132
|
+
'& .ML__center:has(.ML__stretchy)': {
|
|
133
|
+
marginLeft: '0 !important',
|
|
134
|
+
},
|
|
67
135
|
};
|
|
68
136
|
|
|
69
137
|
/**
|
|
@@ -73,6 +141,7 @@ export const placeholderStyles = {
|
|
|
73
141
|
export const commonMqKeyboardStyles = {
|
|
74
142
|
...commonMqFontStyles,
|
|
75
143
|
...longdivStyles,
|
|
144
|
+
...rootIndexStyles,
|
|
76
145
|
...placeholderStyles,
|
|
77
146
|
touchAction: 'manipulation',
|
|
78
147
|
'& math-field': {
|
|
@@ -114,6 +183,7 @@ export const commonKeyboardStyles = commonMqKeyboardStyles;
|
|
|
114
183
|
export default {
|
|
115
184
|
commonMqFontStyles,
|
|
116
185
|
longdivStyles,
|
|
186
|
+
rootIndexStyles,
|
|
117
187
|
supsubStyles,
|
|
118
188
|
commonMqKeyboardStyles,
|
|
119
189
|
placeholderStyles,
|
package/src/mf/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import CommonMqStyles, {
|
|
|
6
6
|
commonMqFontStyles,
|
|
7
7
|
commonMqKeyboardStyles,
|
|
8
8
|
longdivStyles,
|
|
9
|
+
rootIndexStyles,
|
|
9
10
|
supsubStyles,
|
|
10
11
|
placeholderStyles,
|
|
11
12
|
} from './common-styles';
|
|
@@ -21,6 +22,7 @@ export {
|
|
|
21
22
|
commonMqFontStyles,
|
|
22
23
|
commonMqKeyboardStyles,
|
|
23
24
|
longdivStyles,
|
|
25
|
+
rootIndexStyles,
|
|
24
26
|
supsubStyles,
|
|
25
27
|
placeholderStyles,
|
|
26
28
|
};
|
package/src/mf/input.jsx
CHANGED
|
@@ -2,7 +2,13 @@ import React from 'react';
|
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { styled } from '@mui/material/styles';
|
|
4
4
|
import debug from 'debug';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
loadMathLive,
|
|
7
|
+
getMacros,
|
|
8
|
+
MATH_MODE_SPACE,
|
|
9
|
+
applyShadowStyles,
|
|
10
|
+
trackPlaceholderCaret,
|
|
11
|
+
} from '../mathlive-instance';
|
|
6
12
|
import { toMathLive, fromMathLive, keyToAction, KEYSTROKES } from '../latex-bridge';
|
|
7
13
|
|
|
8
14
|
const log = debug('pie-lib:math-input-mathlive:input');
|
|
@@ -49,9 +55,16 @@ export class Input extends React.Component {
|
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
async componentDidMount() {
|
|
58
|
+
// MathLive loads asynchronously, so this method resumes after an await and
|
|
59
|
+
// has to know whether it is still the current mount. React StrictMode calls
|
|
60
|
+
// mount -> unmount -> mount on the SAME instance in dev, so a boolean
|
|
61
|
+
// "unmounted" flag would latch and block the remount from ever creating a
|
|
62
|
+
// field. A generation counter identifies the live mount instead: only the
|
|
63
|
+
// newest one proceeds, and a real unmount invalidates all pending ones.
|
|
64
|
+
const generation = (this.mountGeneration = (this.mountGeneration || 0) + 1);
|
|
52
65
|
const ml = await loadMathLive();
|
|
53
66
|
|
|
54
|
-
if (!ml || !this.holderRef.current) {
|
|
67
|
+
if (generation !== this.mountGeneration || !ml || !this.holderRef.current || this.mathField) {
|
|
55
68
|
return;
|
|
56
69
|
}
|
|
57
70
|
|
|
@@ -64,15 +77,25 @@ export class Input extends React.Component {
|
|
|
64
77
|
|
|
65
78
|
// pie drives input through its own keypad, so suppress MathLive's.
|
|
66
79
|
this.mathField.mathVirtualKeyboardPolicy = 'manual';
|
|
80
|
+
// Without this the spacebar does nothing: MathLive's mathModeSpace
|
|
81
|
+
// defaults to an empty string.
|
|
82
|
+
this.mathField.mathModeSpace = MATH_MODE_SPACE;
|
|
83
|
+
|
|
84
|
+
// The mathfield renders in shadow DOM, so page CSS cannot reach it - the
|
|
85
|
+
// stretchy-accent fix has to be injected there directly.
|
|
86
|
+
applyShadowStyles(this.mathField);
|
|
87
|
+
|
|
88
|
+
// A selected `\placeholder{}` gets no MathLive caret; draw one.
|
|
89
|
+
this.untrackCaret = trackPlaceholderCaret(this.mathField);
|
|
67
90
|
|
|
68
91
|
this.mathField.addEventListener('input', this.onInputEdit);
|
|
69
|
-
|
|
92
|
+
|
|
93
|
+
// Belt and braces: drop anything a previous mount may have left behind.
|
|
94
|
+
this.holderRef.current.replaceChildren(this.mathField);
|
|
70
95
|
|
|
71
96
|
if (this.props.innerRef) {
|
|
72
97
|
this.props.innerRef(this);
|
|
73
98
|
}
|
|
74
|
-
|
|
75
|
-
this.setState?.({});
|
|
76
99
|
}
|
|
77
100
|
|
|
78
101
|
componentDidUpdate(prevProps) {
|
|
@@ -99,6 +122,15 @@ export class Input extends React.Component {
|
|
|
99
122
|
}
|
|
100
123
|
|
|
101
124
|
componentWillUnmount() {
|
|
125
|
+
// Invalidate any mount still awaiting the MathLive load, so it does not
|
|
126
|
+
// append a field into a holder that is going away.
|
|
127
|
+
this.mountGeneration = (this.mountGeneration || 0) + 1;
|
|
128
|
+
|
|
129
|
+
if (this.untrackCaret) {
|
|
130
|
+
this.untrackCaret();
|
|
131
|
+
this.untrackCaret = undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
102
134
|
if (this.mathField) {
|
|
103
135
|
this.mathField.removeEventListener('input', this.onInputEdit);
|
|
104
136
|
this.mathField.remove();
|