@pie-lib/math-input-mathlive 1.1.5-beta.1 → 1.1.5-beta.11
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/lib/__tests__/composites.test.js +255 -0
- package/lib/__tests__/drop-in-compat.test.js +133 -0
- package/lib/__tests__/embedded-fonts.test.js +121 -0
- package/lib/__tests__/index.test.js +118 -0
- package/lib/__tests__/input-imperative.test.js +155 -0
- package/lib/__tests__/keypad-label.test.js +231 -0
- package/lib/__tests__/keypad-render.test.js +206 -0
- package/lib/__tests__/latex-bridge.test.js +386 -0
- package/lib/__tests__/mathlive-instance.test.js +419 -0
- package/lib/__tests__/mount-race.test.js +218 -0
- package/lib/__tests__/static-prompts.test.js +405 -0
- package/lib/__tests__/tmp-markup.test.js +26 -0
- package/lib/keypad/index.js +36 -12
- package/lib/keypad/index.js.map +1 -1
- package/lib/latex-bridge.js +159 -4
- package/lib/latex-bridge.js.map +1 -1
- package/lib/mathlive-instance.js +21 -28
- package/lib/mathlive-instance.js.map +1 -1
- package/lib/mf/common-styles.js +17 -2
- 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 +13 -0
- package/lib/mf/input.js.map +1 -1
- package/lib/mf/static.js +4 -2
- package/lib/mf/static.js.map +1 -1
- package/lib/selection.js +148 -0
- package/lib/selection.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/composites.test.jsx +220 -0
- package/src/__tests__/drop-in-compat.test.jsx +112 -0
- package/src/__tests__/index.test.jsx +109 -0
- package/src/__tests__/input-imperative.test.jsx +144 -0
- package/src/__tests__/input-selection.test.jsx +64 -0
- package/src/__tests__/keypad-label.test.jsx +284 -0
- package/src/__tests__/keypad-render.test.jsx +123 -0
- package/src/__tests__/latex-bridge.test.js +389 -0
- package/src/__tests__/mathlive-instance.test.js +418 -0
- package/src/__tests__/mount-race.test.jsx +142 -0
- package/src/__tests__/selection.test.js +212 -0
- package/src/__tests__/static-prompts.test.jsx +364 -0
- package/src/keypad/index.jsx +18 -3
- package/src/latex-bridge.js +171 -4
- package/src/mathlive-instance.js +27 -29
- package/src/mf/common-styles.js +19 -0
- package/src/mf/index.js +2 -0
- package/src/mf/input.jsx +14 -0
- package/src/mf/static.jsx +4 -1
- package/src/selection.js +146 -0
- package/lib/fonts/embedded.js +0 -120
- package/lib/fonts/index.js +0 -81
- package/lib/fonts/index.js.map +0 -1
- package/scripts/generate-embedded-fonts.js +0 -94
- package/src/fonts/embedded.js +0 -116
- package/src/fonts/index.js +0 -71
package/src/latex-bridge.js
CHANGED
|
@@ -69,6 +69,35 @@ export const DEFAULT_FIELD_ID = 'r1';
|
|
|
69
69
|
*/
|
|
70
70
|
const EMPTY_SLOT = '\\htmlData{pie-empty=1}{\\rule{0.55em}{0.55em}}';
|
|
71
71
|
const EMPTY_GROUP_REGEX = /\{\s*\}/g;
|
|
72
|
+
// The segment key (`\overline{}`). The keypad centres every label in its cell, and
|
|
73
|
+
// a thin bar is a much shorter label than the ray and line keys' stretchy arrows,
|
|
74
|
+
// so by default both its bar and its box sit off from theirs: the bar is lower
|
|
75
|
+
// than the arrows' shafts, and the box higher than theirs. Two zero-width struts
|
|
76
|
+
// fix that, found by rendering the three keys at 14-28px and measuring:
|
|
77
|
+
// - inside the bar, above the box: pushes the bar up to the height of the
|
|
78
|
+
// arrows' shafts (the bar-to-box distance then matches theirs);
|
|
79
|
+
// - before the bar, from the baseline: raises the label's top, which moves the
|
|
80
|
+
// whole label down so the box lands where theirs does.
|
|
81
|
+
const EMPTY_OVERLINE_REGEX = /\\overline\{\s*\}/g;
|
|
82
|
+
const EMPTY_OVERLINE_BAR_STRUT = '\\rule{0em}{0.87em}';
|
|
83
|
+
const EMPTY_OVERLINE_LABEL_STRUT = '\\rule{0em}{1.3em}';
|
|
84
|
+
// Empty delimiter pairs: `\abs{}` is converted to `\left|\right|`, and the
|
|
85
|
+
// parenthesis / bracket keys are `\left(\right)` / `\left[\right]`. None has a
|
|
86
|
+
// `{}` group for the rule above to catch, so they would render as bare fences.
|
|
87
|
+
const EMPTY_FENCE_REGEX = /\\left([([|])\s*\\right([)\]|])/g;
|
|
88
|
+
// The same muted fill as the other slots (`pie-empty`), but a tall rectangle
|
|
89
|
+
// rather than a square. It also sets the fences' height: `\left...\right` stretch
|
|
90
|
+
// to their content, so a taller slot gives taller parentheses.
|
|
91
|
+
//
|
|
92
|
+
// A `\rule` stands on the baseline, but the fences are centred on the math axis
|
|
93
|
+
// (0.25em above it), so an un-lifted box sat high between them. `[-0.25em]`
|
|
94
|
+
// lowers it by (axis - height / 2) = 0.25 - 1 / 2, centring it on the axis.
|
|
95
|
+
//
|
|
96
|
+
// Lowering it alone also shrinks the fences, which size themselves to their
|
|
97
|
+
// content (MathLive switches to short bars). The zero-width `\rule{0em}{1em}`
|
|
98
|
+
// strut keeps the content as tall as before, so the bars keep their height and
|
|
99
|
+
// only the box moves.
|
|
100
|
+
const EMPTY_FENCE_SLOT = '\\rule{0em}{1em}\\htmlData{pie-empty=1}{\\rule[-0.25em]{0.55em}{1em}}';
|
|
72
101
|
|
|
73
102
|
/**
|
|
74
103
|
* Make empty argument slots visible, for static rendering.
|
|
@@ -79,7 +108,11 @@ const EMPTY_GROUP_REGEX = /\{\s*\}/g;
|
|
|
79
108
|
* @param {string} latex
|
|
80
109
|
* @returns {string}
|
|
81
110
|
*/
|
|
82
|
-
export const withVisibleEmptySlots = (latex) =>
|
|
111
|
+
export const withVisibleEmptySlots = (latex) =>
|
|
112
|
+
(latex || '')
|
|
113
|
+
.replace(EMPTY_OVERLINE_REGEX, `${EMPTY_OVERLINE_LABEL_STRUT}\\overline{${EMPTY_OVERLINE_BAR_STRUT}${EMPTY_SLOT}}`)
|
|
114
|
+
.replace(EMPTY_GROUP_REGEX, `{${EMPTY_SLOT}}`)
|
|
115
|
+
.replace(EMPTY_FENCE_REGEX, (_m, open, close) => `\\left${open}${EMPTY_FENCE_SLOT}\\right${close}`);
|
|
83
116
|
|
|
84
117
|
/**
|
|
85
118
|
* Pie commands that take an argument, and their native MathLive equivalents.
|
|
@@ -236,6 +269,138 @@ export const toMathLive = (latex) => {
|
|
|
236
269
|
return out;
|
|
237
270
|
};
|
|
238
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Keypad command names that MathQuill treated as ALIASES of a different command.
|
|
274
|
+
*
|
|
275
|
+
* MathQuill registered `\\divide` as another name for `\\div` (and
|
|
276
|
+
* `\\perpendicular` for `\\perp`), so pressing the key stored the real command.
|
|
277
|
+
* MathLive only knows these names through `PIE_MACROS`, and serialises a macro
|
|
278
|
+
* under its own name - so `\\divide` reached storage verbatim, where MathJax
|
|
279
|
+
* (@pie-lib/math-rendering) has no such command and renders it as red text.
|
|
280
|
+
*
|
|
281
|
+
* Every other keypad command already matches what MathQuill stored (`\\degree`,
|
|
282
|
+
* `\\nsim`, `\\ncong`, `\\nparallel`, `\\square`, `\\napprox`,
|
|
283
|
+
* `\\parallelogram`), and MathJax understands those.
|
|
284
|
+
*/
|
|
285
|
+
const MATHQUILL_ALIASES = {
|
|
286
|
+
'\\divide': '\\div',
|
|
287
|
+
'\\perpendicular': '\\perp',
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// Whole command names only: `\\divideontimes` is a real AMS symbol.
|
|
291
|
+
const MATHQUILL_ALIAS_REGEX = /\\(divide|perpendicular)(?![a-zA-Z])/g;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Primes. MathLive turns a prime that follows a base into a superscript atom and
|
|
295
|
+
* serialises it as `^{\\prime}`, or `^{\\doubleprime}` for the keypad's double
|
|
296
|
+
* prime key (`5''` comes back out as `5^{\\doubleprime}`). MathJax has no
|
|
297
|
+
* `\\doubleprime`, and two adjacent `^{\\prime}` groups - what typing `'` twice
|
|
298
|
+
* produces - are a "double exponent" error. MathQuill stored plain apostrophes,
|
|
299
|
+
* which both MathJax and MathLive read as primes, so store those.
|
|
300
|
+
*
|
|
301
|
+
* Only a superscript made of primes alone is rewritten: `x^{\\prime 2}` keeps
|
|
302
|
+
* its group, since it holds more than primes.
|
|
303
|
+
*/
|
|
304
|
+
const PRIME_SUPERSCRIPT_REGEX = /\^\{((?:\s*\\(?:prime|doubleprime)(?![a-zA-Z]))+)\s*\}/g;
|
|
305
|
+
const BARE_DOUBLEPRIME_REGEX = /\\doubleprime(?![a-zA-Z])/g;
|
|
306
|
+
|
|
307
|
+
const primesToApostrophes = (primes) =>
|
|
308
|
+
// eslint-disable-next-line quotes
|
|
309
|
+
"'".repeat(
|
|
310
|
+
(primes.match(/\\prime(?![a-zA-Z])/g) || []).length + 2 * (primes.match(BARE_DOUBLEPRIME_REGEX) || []).length,
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const BARE_PRIME_COMMAND_REGEX = /^\\(doubleprime|prime)(?![a-zA-Z])/;
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Primes that are *not* in a superscript.
|
|
317
|
+
*
|
|
318
|
+
* MathLive emits bare `\prime` / `\doubleprime` for a `′` or `″` typed or pasted
|
|
319
|
+
* as a character (`5′6″` serialises as `5\prime6\doubleprime`). A bare `\prime`
|
|
320
|
+
* is a full-size glyph in MathJax, while `''` is a superscript, so storing
|
|
321
|
+
* `5\prime6''` drew the single prime visibly larger than each stroke of the
|
|
322
|
+
* double prime. Both become apostrophes, the superscript primes MathQuill
|
|
323
|
+
* stored, so they always match.
|
|
324
|
+
*
|
|
325
|
+
* Inside a script (`x^{\prime 2}`, `x^\prime`) a prime is already a
|
|
326
|
+
* superscript, and an apostrophe there would mean something else, so
|
|
327
|
+
* `\prime` is kept and `\doubleprime` (unknown to MathJax) becomes
|
|
328
|
+
* `\prime\prime`.
|
|
329
|
+
*
|
|
330
|
+
* @param {string} latex
|
|
331
|
+
* @returns {string}
|
|
332
|
+
*/
|
|
333
|
+
const barePrimesToApostrophes = (latex) => {
|
|
334
|
+
if (latex.indexOf('prime') === -1) {
|
|
335
|
+
return latex;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// One entry per open brace: is that group a super/subscript?
|
|
339
|
+
const groups = [];
|
|
340
|
+
let out = '';
|
|
341
|
+
let i = 0;
|
|
342
|
+
|
|
343
|
+
while (i < latex.length) {
|
|
344
|
+
const ch = latex[i];
|
|
345
|
+
|
|
346
|
+
if (ch === '\\') {
|
|
347
|
+
const match = latex.slice(i).match(BARE_PRIME_COMMAND_REGEX);
|
|
348
|
+
|
|
349
|
+
if (match) {
|
|
350
|
+
const isDouble = match[1] === 'doubleprime';
|
|
351
|
+
// `x^\prime`: the command is itself the script.
|
|
352
|
+
const isScriptArgument = /[\^_]\s*$/.test(out);
|
|
353
|
+
const inScript = isScriptArgument || groups.indexOf(true) !== -1;
|
|
354
|
+
|
|
355
|
+
if (inScript) {
|
|
356
|
+
out += isDouble ? '\\prime\\prime' : '\\prime';
|
|
357
|
+
} else {
|
|
358
|
+
// eslint-disable-next-line quotes
|
|
359
|
+
out += isDouble ? "''" : "'";
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
i += match[0].length;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Copy any other command, or an escaped character such as `\{`, whole.
|
|
367
|
+
const command = latex.slice(i).match(/^\\(?:[a-zA-Z]+|.)/);
|
|
368
|
+
const text = command ? command[0] : ch;
|
|
369
|
+
|
|
370
|
+
out += text;
|
|
371
|
+
i += text.length;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (ch === '{') {
|
|
376
|
+
groups.push(/[\^_]\s*$/.test(out));
|
|
377
|
+
} else if (ch === '}') {
|
|
378
|
+
groups.pop();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
out += ch;
|
|
382
|
+
i += 1;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return out;
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Rewrite what MathLive serialises into the form MathQuill stored: alias
|
|
390
|
+
* command names, and primes.
|
|
391
|
+
*
|
|
392
|
+
* @param {string} latex
|
|
393
|
+
* @returns {string}
|
|
394
|
+
*/
|
|
395
|
+
export const toCanonicalCommands = (latex) =>
|
|
396
|
+
typeof latex === 'string'
|
|
397
|
+
? barePrimesToApostrophes(
|
|
398
|
+
latex
|
|
399
|
+
.replace(MATHQUILL_ALIAS_REGEX, (m) => MATHQUILL_ALIASES[m])
|
|
400
|
+
.replace(PRIME_SUPERSCRIPT_REGEX, (_m, primes) => primesToApostrophes(primes)),
|
|
401
|
+
)
|
|
402
|
+
: latex;
|
|
403
|
+
|
|
239
404
|
/**
|
|
240
405
|
* Convert latex out of MathLive back into the stored form, so items authored
|
|
241
406
|
* with the MathLive editor remain readable by @pie-lib/math-rendering and by
|
|
@@ -273,7 +438,9 @@ export const fromMathLive = (latex) => {
|
|
|
273
438
|
out = out.replace(BARE_PLACEHOLDER_REGEX, (_m, content) => content);
|
|
274
439
|
out = out.replace(BARE_PLACEHOLDER_NO_ARG_REGEX, '');
|
|
275
440
|
|
|
276
|
-
|
|
441
|
+
// Store the command MathQuill stored, not the alias - also heals latex that was
|
|
442
|
+
// saved with `\\divide` before the keypad resolved it.
|
|
443
|
+
return toCanonicalCommands(out);
|
|
277
444
|
};
|
|
278
445
|
|
|
279
446
|
/**
|
|
@@ -347,7 +514,7 @@ export const keyToAction = (key) => {
|
|
|
347
514
|
// applied them in sequence; MathLive inserts one latex string, so expand
|
|
348
515
|
// each part and concatenate.
|
|
349
516
|
const parts = Array.isArray(key.command) ? key.command : [key.command];
|
|
350
|
-
const value = parts.map((c) => COMMANDS_WITH_ARGS[c] || c).join('');
|
|
517
|
+
const value = parts.map((c) => COMMANDS_WITH_ARGS[c] || MATHQUILL_ALIASES[c] || c).join('');
|
|
351
518
|
|
|
352
519
|
return { type: 'insert', value };
|
|
353
520
|
}
|
|
@@ -361,4 +528,4 @@ export const keyToAction = (key) => {
|
|
|
361
528
|
return undefined;
|
|
362
529
|
};
|
|
363
530
|
|
|
364
|
-
export { COMMANDS_WITH_ARGS, KEYSTROKES };
|
|
531
|
+
export { COMMANDS_WITH_ARGS, KEYSTROKES, MATHQUILL_ALIASES };
|
package/src/mathlive-instance.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import debug from 'debug';
|
|
2
2
|
import { NEWLINE_EMBED, toMathLive, withVisibleEmptySlots } from './latex-bridge';
|
|
3
|
-
import {
|
|
4
|
-
import { ROOT_INDEX_CSS } from './mf/common-styles';
|
|
3
|
+
import { ROOT_INDEX_CSS, PARALLEL_CSS } from './mf/common-styles';
|
|
5
4
|
|
|
6
5
|
const log = debug('pie-lib:math-input-mathlive:instance');
|
|
7
6
|
|
|
@@ -49,7 +48,7 @@ const absoluteFontsDirectory = (dir) => {
|
|
|
49
48
|
};
|
|
50
49
|
|
|
51
50
|
/**
|
|
52
|
-
*
|
|
51
|
+
* Version-pinned CDN fallback for the KaTeX fonts.
|
|
53
52
|
*
|
|
54
53
|
* Some symbols are composed from Private Use Area glyphs that exist only in
|
|
55
54
|
* those fonts: `\\neq` is U+E020 (a negation slash) overlapped on `=`, and
|
|
@@ -57,10 +56,9 @@ const absoluteFontsDirectory = (dir) => {
|
|
|
57
56
|
* an arbitrary glyph for U+E020, so those keys render as garbage rather than
|
|
58
57
|
* simply falling back to a plainer face.
|
|
59
58
|
*
|
|
60
|
-
* The
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* always match the engine.
|
|
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.
|
|
64
62
|
*/
|
|
65
63
|
const FALLBACK_MATHLIVE_VERSION = '0.110.0';
|
|
66
64
|
|
|
@@ -74,17 +72,15 @@ const cdnFontsDirectory = () => {
|
|
|
74
72
|
/**
|
|
75
73
|
* Tell the package where the MathLive fonts are served from.
|
|
76
74
|
*
|
|
77
|
-
*
|
|
78
|
-
* so nothing has to be served or configured. Call this before `loadMathLive()`
|
|
79
|
-
* to point MathLive at your own copy instead - worth doing if the host already
|
|
80
|
-
* serves the KaTeX woff2 files and would rather not carry them twice.
|
|
75
|
+
* Call before `loadMathLive()`.
|
|
81
76
|
*
|
|
82
|
-
* - a path/URL: served by you
|
|
83
|
-
* - `null`: skip font loading
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
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
|
|
81
|
+
*
|
|
82
|
+
* The fonts ship inside the `mathlive` package (`node_modules/mathlive/fonts`)
|
|
83
|
+
* ready to copy into whatever the host app serves statically.
|
|
88
84
|
*
|
|
89
85
|
* @param {string|null} [dir]
|
|
90
86
|
*/
|
|
@@ -120,6 +116,9 @@ export const PIE_MACROS = {
|
|
|
120
116
|
// Argument-less symbols. Atomic is correct here: backspace should remove the
|
|
121
117
|
// whole glyph, and there is nothing to type into.
|
|
122
118
|
parallelogram: '\\htmlData{pie-macro=parallelogram}{\\unicode{"25B1}}',
|
|
119
|
+
// Same glyph as the built-in `\\parallel` (U+2225), tagged so it can be sized -
|
|
120
|
+
// see `parallelStyles`.
|
|
121
|
+
parallel: '\\htmlData{pie-macro=parallel}{\\unicode{"2225}}',
|
|
123
122
|
napprox: '\\not\\approx',
|
|
124
123
|
nsim: '\\not\\sim',
|
|
125
124
|
ncong: '\\not\\cong',
|
|
@@ -167,22 +166,18 @@ let loadWarned = false;
|
|
|
167
166
|
|
|
168
167
|
/** Apply post-load configuration exactly once. */
|
|
169
168
|
const configureLoaded = () => {
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
if (fontsDirectory === undefined && !registerEmbeddedFonts()) {
|
|
169
|
+
// Always replace MathLive's relative `'./fonts/'` default: it throws
|
|
170
|
+
// "Invalid base URL" when bundled. Use the host's copy if one was configured,
|
|
171
|
+
// otherwise the version-pinned CDN.
|
|
172
|
+
if (fontsDirectory === undefined) {
|
|
175
173
|
log(
|
|
176
|
-
'
|
|
177
|
-
'Serve the fonts yourself and call configureFonts("/path/to/fonts") to avoid the network request
|
|
174
|
+
'no fontsDirectory configured - falling back to the CDN (%s). ' +
|
|
175
|
+
'Serve the fonts yourself and call configureFonts("/path/to/fonts") to avoid the network request, ' +
|
|
176
|
+
'or configureFonts(null) to opt out.',
|
|
178
177
|
cdnFontsDirectory(),
|
|
179
178
|
);
|
|
180
179
|
}
|
|
181
180
|
|
|
182
|
-
// Always replace MathLive's relative `'./fonts/'` default: it throws
|
|
183
|
-
// "Invalid base URL" when bundled. The host's copy if one was configured,
|
|
184
|
-
// otherwise the CDN, which is now only reached if the embedded fonts could not
|
|
185
|
-
// be registered.
|
|
186
181
|
mathlive.MathfieldElement.fontsDirectory =
|
|
187
182
|
fontsDirectory === undefined ? cdnFontsDirectory() : absoluteFontsDirectory(fontsDirectory);
|
|
188
183
|
|
|
@@ -244,7 +239,10 @@ const PLACEHOLDER_CARET_CSS = [
|
|
|
244
239
|
// written for static markup in the host page, which cannot cross the shadow
|
|
245
240
|
// boundary.
|
|
246
241
|
const SHADOW_CSS =
|
|
247
|
-
'.ML__latex .ML__center:has(.ML__stretchy){margin-left:0 !important}' +
|
|
242
|
+
'.ML__latex .ML__center:has(.ML__stretchy){margin-left:0 !important}' +
|
|
243
|
+
ROOT_INDEX_CSS +
|
|
244
|
+
PARALLEL_CSS +
|
|
245
|
+
PLACEHOLDER_CARET_CSS;
|
|
248
246
|
|
|
249
247
|
/**
|
|
250
248
|
* Keep {@link PLACEHOLDER_CARET_CLASS} in step with the selection.
|
package/src/mf/common-styles.js
CHANGED
|
@@ -75,6 +75,24 @@ export const rootIndexStyles = {
|
|
|
75
75
|
*/
|
|
76
76
|
export const ROOT_INDEX_CSS = `${ROOT_INDEX_SELECTOR}{font-size:${ROOT_INDEX_FONT_SIZE} !important}`;
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Size of the parallel sign (`\parallel`, tagged `data-pie-macro="parallel"` by
|
|
80
|
+
* `PIE_MACROS`). The legacy editor enlarged it (`updateSpans` set 32px) because
|
|
81
|
+
* the bars read too small next to the letters of `AB ∥ CD`; MathLive draws it at
|
|
82
|
+
* the surrounding size, so it is scaled up here. Not applied to keypad labels.
|
|
83
|
+
*/
|
|
84
|
+
const PARALLEL_SELECTOR = '[data-pie-macro="parallel"]';
|
|
85
|
+
const PARALLEL_FONT_SIZE = '1.4em';
|
|
86
|
+
|
|
87
|
+
export const parallelStyles = {
|
|
88
|
+
[`& ${PARALLEL_SELECTOR}`]: {
|
|
89
|
+
fontSize: PARALLEL_FONT_SIZE,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** The same rule as a CSS string, for a live mathfield's shadow root. */
|
|
94
|
+
export const PARALLEL_CSS = `${PARALLEL_SELECTOR}{font-size:${PARALLEL_FONT_SIZE}}`;
|
|
95
|
+
|
|
78
96
|
/**
|
|
79
97
|
* Superscript/subscript sizing. MathLive follows TeX metrics for scripts, so the
|
|
80
98
|
* MathQuill-era corrections are gone; the radical index is the one place where a
|
|
@@ -82,6 +100,7 @@ export const ROOT_INDEX_CSS = `${ROOT_INDEX_SELECTOR}{font-size:${ROOT_INDEX_FON
|
|
|
82
100
|
*/
|
|
83
101
|
export const supsubStyles = {
|
|
84
102
|
...rootIndexStyles,
|
|
103
|
+
...parallelStyles,
|
|
85
104
|
};
|
|
86
105
|
|
|
87
106
|
/** Empty placeholder boxes (answer blocks / open slots). */
|
package/src/mf/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import CommonMqStyles, {
|
|
|
7
7
|
commonMqKeyboardStyles,
|
|
8
8
|
longdivStyles,
|
|
9
9
|
rootIndexStyles,
|
|
10
|
+
parallelStyles,
|
|
10
11
|
supsubStyles,
|
|
11
12
|
placeholderStyles,
|
|
12
13
|
} from './common-styles';
|
|
@@ -23,6 +24,7 @@ export {
|
|
|
23
24
|
commonMqKeyboardStyles,
|
|
24
25
|
longdivStyles,
|
|
25
26
|
rootIndexStyles,
|
|
27
|
+
parallelStyles,
|
|
26
28
|
supsubStyles,
|
|
27
29
|
placeholderStyles,
|
|
28
30
|
};
|
package/src/mf/input.jsx
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
trackPlaceholderCaret,
|
|
11
11
|
} from '../mathlive-instance';
|
|
12
12
|
import { toMathLive, fromMathLive, keyToAction, KEYSTROKES } from '../latex-bridge';
|
|
13
|
+
import { trackWholeAtomSelection } from '../selection';
|
|
13
14
|
|
|
14
15
|
const log = debug('pie-lib:math-input-mathlive:input');
|
|
15
16
|
|
|
@@ -80,6 +81,11 @@ export class Input extends React.Component {
|
|
|
80
81
|
// Without this the spacebar does nothing: MathLive's mathModeSpace
|
|
81
82
|
// defaults to an empty string.
|
|
82
83
|
this.mathField.mathModeSpace = MATH_MODE_SPACE;
|
|
84
|
+
// After the exponent key the cursor must stay in the exponent until it is moved
|
|
85
|
+
// MathLive's `smartSuperscript` (on by
|
|
86
|
+
// default) jumps out of an empty superscript after a single digit, which breaks
|
|
87
|
+
// typing x^23 or 10^6 and is inconsistent with non-digit exponents.
|
|
88
|
+
this.mathField.smartSuperscript = false;
|
|
83
89
|
|
|
84
90
|
// The mathfield renders in shadow DOM, so page CSS cannot reach it - the
|
|
85
91
|
// stretchy-accent fix has to be injected there directly.
|
|
@@ -88,6 +94,9 @@ export class Input extends React.Component {
|
|
|
88
94
|
// A selected `\placeholder{}` gets no MathLive caret; draw one.
|
|
89
95
|
this.untrackCaret = trackPlaceholderCaret(this.mathField);
|
|
90
96
|
|
|
97
|
+
// Shift+Arrow would otherwise select half a fraction; snap to whole atoms.
|
|
98
|
+
this.untrackSelection = trackWholeAtomSelection(this.mathField);
|
|
99
|
+
|
|
91
100
|
this.mathField.addEventListener('input', this.onInputEdit);
|
|
92
101
|
|
|
93
102
|
// Belt and braces: drop anything a previous mount may have left behind.
|
|
@@ -131,6 +140,11 @@ export class Input extends React.Component {
|
|
|
131
140
|
this.untrackCaret = undefined;
|
|
132
141
|
}
|
|
133
142
|
|
|
143
|
+
if (this.untrackSelection) {
|
|
144
|
+
this.untrackSelection();
|
|
145
|
+
this.untrackSelection = undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
134
148
|
if (this.mathField) {
|
|
135
149
|
this.mathField.removeEventListener('input', this.onInputEdit);
|
|
136
150
|
this.mathField.remove();
|
package/src/mf/static.jsx
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
trackPlaceholderCaret,
|
|
12
12
|
} from '../mathlive-instance';
|
|
13
13
|
import { toMathLive, fromMathLive, fieldIds } from '../latex-bridge';
|
|
14
|
-
import { placeholderStyles, rootIndexStyles } from './common-styles';
|
|
14
|
+
import { placeholderStyles, rootIndexStyles, parallelStyles } from './common-styles';
|
|
15
15
|
|
|
16
16
|
const log = debug('pie-lib:math-input-mathlive:static');
|
|
17
17
|
|
|
@@ -22,6 +22,7 @@ const Holder = styled('span')({
|
|
|
22
22
|
// Readable radical index - display mode renders in the light DOM, so the
|
|
23
23
|
// shadow-root copy of this rule does not reach it.
|
|
24
24
|
...rootIndexStyles,
|
|
25
|
+
...parallelStyles,
|
|
25
26
|
'& math-field': {
|
|
26
27
|
display: 'inline-block',
|
|
27
28
|
border: 'none',
|
|
@@ -101,6 +102,8 @@ export default class Static extends React.Component {
|
|
|
101
102
|
// Without this the spacebar does nothing: MathLive's mathModeSpace
|
|
102
103
|
// defaults to an empty string.
|
|
103
104
|
this.mathField.mathModeSpace = MATH_MODE_SPACE;
|
|
105
|
+
// Same as the editor: stay in the exponent after a digit (see mf/input.jsx).
|
|
106
|
+
this.mathField.smartSuperscript = false;
|
|
104
107
|
this.mathField.value = toMathLive(this.props.latex);
|
|
105
108
|
|
|
106
109
|
// The mathfield renders in shadow DOM, so page CSS cannot reach it - the
|
package/src/selection.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
|
|
3
|
+
const log = debug('pie-lib:math-input-mathlive:selection');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Widen a selection so it covers whole atoms of a single parent.
|
|
7
|
+
*
|
|
8
|
+
* MathLive extends a selection over *flat offsets*, so Shift+Arrow walks into
|
|
9
|
+
* a fraction instead of over it: from before `2x` in `5x^2+2x+\frac{\pi}{2}-4`,
|
|
10
|
+
* five presses select `2x+` plus the `\pi` of the numerator, and copying gives
|
|
11
|
+
* `2x+\pi`. MathQuill treated a fraction (root, superscript...) as one unit,
|
|
12
|
+
* which is what authors expect.
|
|
13
|
+
*
|
|
14
|
+
* The tree is recovered from MathLive's public `getElementInfo(offset)`, where
|
|
15
|
+
* offset `i` describes the atom ending at caret position `i`:
|
|
16
|
+
*
|
|
17
|
+
* - a composite atom (fraction, root, script) owns the offset right *after*
|
|
18
|
+
* its children; the children are one level deeper;
|
|
19
|
+
* - each branch (numerator, denominator, radicand) opens with an empty-latex
|
|
20
|
+
* "start" atom.
|
|
21
|
+
*
|
|
22
|
+
* The rule: find the shallowest depth the selection touches, counting the caret
|
|
23
|
+
* it starts from. If the range crosses into a sibling branch at that depth, the
|
|
24
|
+
* real common parent is one level up. Then push both ends out of anything
|
|
25
|
+
* deeper. A script atom (`^2`) is kept with its base, since MathLive stores
|
|
26
|
+
* `x^2` as `x` followed by a separate `^2` atom.
|
|
27
|
+
*
|
|
28
|
+
* Pure, so it can be tested without a browser.
|
|
29
|
+
*
|
|
30
|
+
* @param {number} start
|
|
31
|
+
* @param {number} end
|
|
32
|
+
* @param {function(number): {depth?: number, latex?: string}} info
|
|
33
|
+
* @param {number} lastOffset
|
|
34
|
+
* @returns {[number, number]} the widened range, start <= end
|
|
35
|
+
*/
|
|
36
|
+
export const expandRange = (start, end, info, lastOffset) => {
|
|
37
|
+
const at = (i) => info(i) || {};
|
|
38
|
+
const depth = (i) => at(i).depth || 0;
|
|
39
|
+
const isBranchStart = (i) => depth(i) > 0 && at(i).latex === '';
|
|
40
|
+
|
|
41
|
+
if (end < start) {
|
|
42
|
+
[start, end] = [end, start];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (end - start < 1) {
|
|
46
|
+
return [start, end];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let target = Infinity;
|
|
50
|
+
|
|
51
|
+
for (let i = start; i <= end; i++) {
|
|
52
|
+
target = Math.min(target, depth(i));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (let i = start + 1; i <= end; i++) {
|
|
56
|
+
if (depth(i) === target && isBranchStart(i)) {
|
|
57
|
+
target -= 1;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
target = Math.max(target, 0);
|
|
63
|
+
|
|
64
|
+
while (start > 0 && depth(start) > target) {
|
|
65
|
+
start -= 1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
while (end < lastOffset && depth(end) > target) {
|
|
69
|
+
end += 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Keep a superscript / subscript with its base.
|
|
73
|
+
let first = start + 1;
|
|
74
|
+
|
|
75
|
+
while (first <= end && depth(first) > target) {
|
|
76
|
+
first += 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (start > 0 && first <= end && /^[\^_]/.test(at(first).latex || '') && depth(start) === target) {
|
|
80
|
+
start -= 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return [start, end];
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Keep a mathfield's selection snapped to whole atoms (see {@link expandRange}).
|
|
88
|
+
*
|
|
89
|
+
* This fixes both what the user sees highlighted and what copy/cut put on the
|
|
90
|
+
* clipboard, since both come from the selection. The selection direction is
|
|
91
|
+
* preserved, so further Shift+Arrow presses keep extending the same end.
|
|
92
|
+
*
|
|
93
|
+
* @param {HTMLElement} mathField a MathLive `<math-field>`
|
|
94
|
+
* @returns {function|undefined} teardown, to call on unmount
|
|
95
|
+
*/
|
|
96
|
+
export const trackWholeAtomSelection = (mathField) => {
|
|
97
|
+
if (!mathField || typeof mathField.addEventListener !== 'function') {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let applying = false;
|
|
102
|
+
|
|
103
|
+
const update = () => {
|
|
104
|
+
if (applying) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
if (mathField.selectionIsCollapsed || typeof mathField.getElementInfo !== 'function') {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const selection = mathField.selection;
|
|
114
|
+
const range = selection && selection.ranges && selection.ranges[0];
|
|
115
|
+
|
|
116
|
+
if (!range) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const [start, end] = range;
|
|
121
|
+
const lo = Math.min(start, end);
|
|
122
|
+
const hi = Math.max(start, end);
|
|
123
|
+
const [nextStart, nextEnd] = expandRange(
|
|
124
|
+
lo,
|
|
125
|
+
hi,
|
|
126
|
+
(i) => mathField.getElementInfo(i),
|
|
127
|
+
mathField.lastOffset,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (nextStart === lo && nextEnd === hi) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
applying = true;
|
|
135
|
+
mathField.selection = { ranges: [[nextStart, nextEnd]], direction: selection.direction };
|
|
136
|
+
} catch (e) {
|
|
137
|
+
log('could not normalise the selection: %s', e && e.message);
|
|
138
|
+
} finally {
|
|
139
|
+
applying = false;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
mathField.addEventListener('selection-change', update);
|
|
144
|
+
|
|
145
|
+
return () => mathField.removeEventListener('selection-change', update);
|
|
146
|
+
};
|