@pie-lib/math-input-mathlive 1.1.4 → 1.1.5-beta.10
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/__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 +50 -28
- 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 +207 -7
- package/lib/latex-bridge.js.map +1 -1
- package/lib/mathlive-instance.js +176 -12
- package/lib/mathlive-instance.js.map +1 -1
- package/lib/mf/common-styles.js +89 -10
- package/lib/mf/common-styles.js.map +1 -1
- package/lib/mf/index.js +12 -0
- package/lib/mf/index.js.map +1 -1
- package/lib/mf/input.js +34 -5
- package/lib/mf/input.js.map +1 -1
- package/lib/mf/static.js +107 -7
- package/lib/mf/static.js.map +1 -1
- package/package.json +3 -4
- package/src/__tests__/composites.test.jsx +220 -0
- package/src/__tests__/keypad-label.test.jsx +229 -2
- package/src/__tests__/keypad-render.test.jsx +123 -0
- package/src/__tests__/latex-bridge.test.js +159 -0
- package/src/__tests__/mathlive-instance.test.js +237 -0
- package/src/__tests__/mount-race.test.jsx +142 -0
- package/src/__tests__/static-prompts.test.jsx +364 -0
- package/src/keypad/index.jsx +33 -17
- package/src/keys/geometry.js +7 -2
- package/src/keys/misc.js +8 -2
- package/src/latex-bridge.js +223 -7
- package/src/mathlive-instance.js +196 -11
- package/src/mf/common-styles.js +97 -8
- package/src/mf/index.js +4 -0
- package/src/mf/input.jsx +42 -5
- package/src/mf/static.jsx +124 -6
- package/CHANGELOG.md +0 -30
- package/LICENSE.md +0 -5
package/src/latex-bridge.js
CHANGED
|
@@ -55,12 +55,49 @@ 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;
|
|
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}}';
|
|
64
101
|
|
|
65
102
|
/**
|
|
66
103
|
* Make empty argument slots visible, for static rendering.
|
|
@@ -71,7 +108,11 @@ const EMPTY_GROUP_REGEX = /\{\s*\}/g;
|
|
|
71
108
|
* @param {string} latex
|
|
72
109
|
* @returns {string}
|
|
73
110
|
*/
|
|
74
|
-
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}`);
|
|
75
116
|
|
|
76
117
|
/**
|
|
77
118
|
* Pie commands that take an argument, and their native MathLive equivalents.
|
|
@@ -106,6 +147,40 @@ export const toNativeCommands = (latex) => {
|
|
|
106
147
|
out = replaceAbs(out);
|
|
107
148
|
out = out.replace(/\\longdiv\{/g, '\\enclose{longdiv}{');
|
|
108
149
|
out = out.replace(/\\overarc\{/g, '\\overparen{');
|
|
150
|
+
out = escapeBareComments(out);
|
|
151
|
+
|
|
152
|
+
return out;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Escape an unescaped `%`.
|
|
157
|
+
*
|
|
158
|
+
* In real LaTeX `%` starts a comment, so MathLive discards it *and everything
|
|
159
|
+
* after it*: `50%` renders as `50`, and `x%y` as just `x`. MathQuill had no
|
|
160
|
+
* such notion - it treated `%` as a symbol - so latex from anywhere outside the
|
|
161
|
+
* keypad may still carry a bare one.
|
|
162
|
+
*
|
|
163
|
+
* Only a `%` preceded by an even number of backslashes is bare: `\%` is already
|
|
164
|
+
* escaped, while the `%` in `\\%` (a line break, then a comment) is not.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} latex
|
|
167
|
+
* @returns {string}
|
|
168
|
+
*/
|
|
169
|
+
const escapeBareComments = (latex) => {
|
|
170
|
+
if (latex.indexOf('%') === -1) {
|
|
171
|
+
return latex;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let out = '';
|
|
175
|
+
// Consecutive backslashes immediately before the current character.
|
|
176
|
+
let backslashes = 0;
|
|
177
|
+
|
|
178
|
+
for (let i = 0; i < latex.length; i++) {
|
|
179
|
+
const ch = latex[i];
|
|
180
|
+
|
|
181
|
+
out += ch === '%' && backslashes % 2 === 0 ? '\\%' : ch;
|
|
182
|
+
backslashes = ch === '\\' ? backslashes + 1 : 0;
|
|
183
|
+
}
|
|
109
184
|
|
|
110
185
|
return out;
|
|
111
186
|
};
|
|
@@ -194,6 +269,138 @@ export const toMathLive = (latex) => {
|
|
|
194
269
|
return out;
|
|
195
270
|
};
|
|
196
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
|
+
|
|
197
404
|
/**
|
|
198
405
|
* Convert latex out of MathLive back into the stored form, so items authored
|
|
199
406
|
* with the MathLive editor remain readable by @pie-lib/math-rendering and by
|
|
@@ -231,7 +438,9 @@ export const fromMathLive = (latex) => {
|
|
|
231
438
|
out = out.replace(BARE_PLACEHOLDER_REGEX, (_m, content) => content);
|
|
232
439
|
out = out.replace(BARE_PLACEHOLDER_NO_ARG_REGEX, '');
|
|
233
440
|
|
|
234
|
-
|
|
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);
|
|
235
444
|
};
|
|
236
445
|
|
|
237
446
|
/**
|
|
@@ -240,8 +449,15 @@ export const fromMathLive = (latex) => {
|
|
|
240
449
|
* placeholders in the inserted latex.
|
|
241
450
|
*/
|
|
242
451
|
const COMMANDS_WITH_ARGS = {
|
|
452
|
+
// An explicit "blank over blank" key: both slots start empty.
|
|
243
453
|
'\\frac': '\\frac{#?}{#?}',
|
|
244
|
-
'
|
|
454
|
+
// Division, which must behave like MathQuill's `/`: whatever precedes the
|
|
455
|
+
// cursor becomes the numerator and the caret lands in the denominator. `#@`
|
|
456
|
+
// is MathLive's token for "the selection, or the item before the cursor" -
|
|
457
|
+
// this is exactly the template MathLive binds to the `/` key itself. Using
|
|
458
|
+
// `#?` here instead produced an empty fraction and left the typed number
|
|
459
|
+
// stranded outside it.
|
|
460
|
+
'/': '\\frac{#@}{#?}',
|
|
245
461
|
'\\sqrt': '\\sqrt{#?}',
|
|
246
462
|
'\\nthroot': '\\sqrt[#?]{#?}',
|
|
247
463
|
'\\overline': '\\overline{#?}',
|
|
@@ -298,7 +514,7 @@ export const keyToAction = (key) => {
|
|
|
298
514
|
// applied them in sequence; MathLive inserts one latex string, so expand
|
|
299
515
|
// each part and concatenate.
|
|
300
516
|
const parts = Array.isArray(key.command) ? key.command : [key.command];
|
|
301
|
-
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('');
|
|
302
518
|
|
|
303
519
|
return { type: 'insert', value };
|
|
304
520
|
}
|
|
@@ -312,4 +528,4 @@ export const keyToAction = (key) => {
|
|
|
312
528
|
return undefined;
|
|
313
529
|
};
|
|
314
530
|
|
|
315
|
-
export { COMMANDS_WITH_ARGS, KEYSTROKES };
|
|
531
|
+
export { COMMANDS_WITH_ARGS, KEYSTROKES, MATHQUILL_ALIASES };
|
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, PARALLEL_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.
|
|
@@ -78,6 +116,9 @@ export const PIE_MACROS = {
|
|
|
78
116
|
// Argument-less symbols. Atomic is correct here: backspace should remove the
|
|
79
117
|
// whole glyph, and there is nothing to type into.
|
|
80
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}}',
|
|
81
122
|
napprox: '\\not\\approx',
|
|
82
123
|
nsim: '\\not\\sim',
|
|
83
124
|
ncong: '\\not\\cong',
|
|
@@ -126,21 +167,165 @@ let loadWarned = false;
|
|
|
126
167
|
/** Apply post-load configuration exactly once. */
|
|
127
168
|
const configureLoaded = () => {
|
|
128
169
|
// Always replace MathLive's relative `'./fonts/'` default: it throws
|
|
129
|
-
// "Invalid base URL" when bundled.
|
|
130
|
-
//
|
|
170
|
+
// "Invalid base URL" when bundled. Use the host's copy if one was configured,
|
|
171
|
+
// otherwise the version-pinned CDN.
|
|
131
172
|
if (fontsDirectory === undefined) {
|
|
132
173
|
log(
|
|
133
|
-
'no fontsDirectory configured -
|
|
134
|
-
'
|
|
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.',
|
|
177
|
+
cdnFontsDirectory(),
|
|
135
178
|
);
|
|
136
179
|
}
|
|
137
180
|
|
|
138
181
|
mathlive.MathfieldElement.fontsDirectory =
|
|
139
|
-
fontsDirectory === undefined ?
|
|
182
|
+
fontsDirectory === undefined ? cdnFontsDirectory() : absoluteFontsDirectory(fontsDirectory);
|
|
140
183
|
|
|
141
184
|
injectCoreStylesheet();
|
|
142
185
|
};
|
|
143
186
|
|
|
187
|
+
/**
|
|
188
|
+
* CSS that has to live *inside* a mathfield's shadow root.
|
|
189
|
+
*
|
|
190
|
+
* A live `<math-field>` renders in shadow DOM (`attachShadow({mode:'open'})`),
|
|
191
|
+
* so nothing in the host page's stylesheets reaches it. The stretchy-accent
|
|
192
|
+
* problem therefore needs fixing twice: once for static markup (see
|
|
193
|
+
* `placeholderStyles`) and once here for the editor.
|
|
194
|
+
*
|
|
195
|
+
* MathLive centres an accent with `margin-left: <half the content width>` as an
|
|
196
|
+
* inline style. For a stretchy accent that also shrinks its containing block
|
|
197
|
+
* (`.ML__center` is position:relative), so the arc ends up half-width and offset
|
|
198
|
+
* - visibly failing to span the text underneath.
|
|
199
|
+
*
|
|
200
|
+
* `:has(.ML__stretchy)` keeps the fix to stretchy accents; single-glyph accents
|
|
201
|
+
* (`\hat{x}`, `\vec{x}`) still need their centring offset.
|
|
202
|
+
*/
|
|
203
|
+
/**
|
|
204
|
+
* Marker put on the host `<math-field>` while a `\placeholder{}` is selected,
|
|
205
|
+
* so the caret rules below apply to a placeholder and nothing else.
|
|
206
|
+
*/
|
|
207
|
+
export const PLACEHOLDER_CARET_CLASS = 'pie-caret-in-placeholder';
|
|
208
|
+
|
|
209
|
+
/** What MathLive serialises a `#?` slot as. */
|
|
210
|
+
const PLACEHOLDER_LATEX = '\\placeholder{}';
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A blinking caret inside a selected placeholder box.
|
|
214
|
+
*
|
|
215
|
+
* A keypad key such as `\frac` inserts `\frac{#?}{#?}`. MathLive turns each
|
|
216
|
+
* `#?` into a `\placeholder{}` atom and *selects* the first one. That atom is a
|
|
217
|
+
* leaf - the offsets around it are "before" and "after", there is no position
|
|
218
|
+
* inside it - so MathLive renders **no caret element at all** and shows only
|
|
219
|
+
* the selection highlight. The box therefore looked focused (blue) but dead,
|
|
220
|
+
* where MathQuill used to blink a cursor inside its empty box.
|
|
221
|
+
*
|
|
222
|
+
* So the caret is drawn here. `:not(:has(.ML__selected))` picks the innermost
|
|
223
|
+
* selected element (MathLive puts the class on a wrapper *and* the glyph span,
|
|
224
|
+
* which would otherwise give two carets), and the `ML__caret-blink` keyframes
|
|
225
|
+
* plus `--_caret-color` are MathLive's own, already in this shadow root.
|
|
226
|
+
*
|
|
227
|
+
* Typing still replaces the whole placeholder, which for an empty box is
|
|
228
|
+
* indistinguishable from inserting at the caret.
|
|
229
|
+
*/
|
|
230
|
+
const PLACEHOLDER_CARET_CSS = [
|
|
231
|
+
`:host(.${PLACEHOLDER_CARET_CLASS}) .ML__selected:not(:has(.ML__selected)){position:relative}`,
|
|
232
|
+
`:host(.${PLACEHOLDER_CARET_CLASS}) .ML__selected:not(:has(.ML__selected))::after{`,
|
|
233
|
+
'content:"";position:absolute;left:50%;top:12%;height:76%;',
|
|
234
|
+
'border-right:2px solid var(--_caret-color, currentColor);',
|
|
235
|
+
'animation:ML__caret-blink 1.05s step-end forwards infinite}',
|
|
236
|
+
].join('');
|
|
237
|
+
|
|
238
|
+
// A live mathfield needs the readable radical index too: `rootIndexStyles` is
|
|
239
|
+
// written for static markup in the host page, which cannot cross the shadow
|
|
240
|
+
// boundary.
|
|
241
|
+
const SHADOW_CSS =
|
|
242
|
+
'.ML__latex .ML__center:has(.ML__stretchy){margin-left:0 !important}' +
|
|
243
|
+
ROOT_INDEX_CSS +
|
|
244
|
+
PARALLEL_CSS +
|
|
245
|
+
PLACEHOLDER_CARET_CSS;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Keep {@link PLACEHOLDER_CARET_CLASS} in step with the selection.
|
|
249
|
+
*
|
|
250
|
+
* Gating on the class is what keeps the caret specific to placeholders: an
|
|
251
|
+
* ordinary selection (`x+1`) must not sprout one. MathLive fires
|
|
252
|
+
* `selection-change` for every caret move, including the one that lands on the
|
|
253
|
+
* placeholder straight after an insert.
|
|
254
|
+
*
|
|
255
|
+
* @param {HTMLElement} mathField
|
|
256
|
+
* @returns {function|undefined} teardown, to call on unmount
|
|
257
|
+
*/
|
|
258
|
+
export const trackPlaceholderCaret = (mathField) => {
|
|
259
|
+
if (!mathField || typeof mathField.addEventListener !== 'function') {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const update = () => {
|
|
264
|
+
let selected = '';
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
// A collapsed selection already has a real MathLive caret.
|
|
268
|
+
if (!mathField.selectionIsCollapsed && mathField.selection) {
|
|
269
|
+
selected = mathField.getValue(mathField.selection, 'latex');
|
|
270
|
+
}
|
|
271
|
+
} catch (e) {
|
|
272
|
+
log('could not read the selection: %s', e && e.message);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (mathField.classList) {
|
|
276
|
+
mathField.classList.toggle(PLACEHOLDER_CARET_CLASS, selected === PLACEHOLDER_LATEX);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
mathField.addEventListener('selection-change', update);
|
|
281
|
+
update();
|
|
282
|
+
|
|
283
|
+
return () => mathField.removeEventListener('selection-change', update);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Apply {@link SHADOW_CSS} inside a mathfield's shadow root.
|
|
288
|
+
*
|
|
289
|
+
* Uses `adoptedStyleSheets` when available (which is how MathLive loads its own
|
|
290
|
+
* stylesheets) and falls back to appending a <style> node.
|
|
291
|
+
*
|
|
292
|
+
* @param {HTMLElement} mathField
|
|
293
|
+
*/
|
|
294
|
+
export const applyShadowStyles = (mathField) => {
|
|
295
|
+
const root = mathField && mathField.shadowRoot;
|
|
296
|
+
|
|
297
|
+
if (!root) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
if ('adoptedStyleSheets' in root && typeof CSSStyleSheet !== 'undefined') {
|
|
303
|
+
// Do not re-add on a re-render.
|
|
304
|
+
if (root.__pieAccentSheet) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const sheet = new CSSStyleSheet();
|
|
309
|
+
|
|
310
|
+
sheet.replaceSync(SHADOW_CSS);
|
|
311
|
+
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
|
|
312
|
+
root.__pieAccentSheet = sheet;
|
|
313
|
+
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!root.querySelector('style[data-pie-accent]')) {
|
|
318
|
+
const style = document.createElement('style');
|
|
319
|
+
|
|
320
|
+
style.setAttribute('data-pie-accent', '');
|
|
321
|
+
style.textContent = SHADOW_CSS;
|
|
322
|
+
root.appendChild(style);
|
|
323
|
+
}
|
|
324
|
+
} catch (e) {
|
|
325
|
+
log('could not apply shadow styles to the mathfield: %s', e && e.message);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
144
329
|
/**
|
|
145
330
|
* Ensure MathLive's "core" stylesheet is in the document.
|
|
146
331
|
*
|
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,66 @@ 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.
|
|
61
|
+
*/
|
|
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
|
+
* 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.
|
|
45
83
|
*/
|
|
46
|
-
|
|
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
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Superscript/subscript sizing. MathLive follows TeX metrics for scripts, so the
|
|
98
|
+
* MathQuill-era corrections are gone; the radical index is the one place where a
|
|
99
|
+
* TeX-correct size reads too small in the authoring UI.
|
|
100
|
+
*/
|
|
101
|
+
export const supsubStyles = {
|
|
102
|
+
...rootIndexStyles,
|
|
103
|
+
...parallelStyles,
|
|
104
|
+
};
|
|
47
105
|
|
|
48
106
|
/** Empty placeholder boxes (answer blocks / open slots). */
|
|
49
107
|
export const placeholderStyles = {
|
|
@@ -57,13 +115,42 @@ export const placeholderStyles = {
|
|
|
57
115
|
'& [data-pie-empty]': {
|
|
58
116
|
opacity: 0.5,
|
|
59
117
|
},
|
|
60
|
-
// Stretchy accents
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
118
|
+
// Stretchy accents (\overarc, \overleftrightarrow, \overrightarrow) render an
|
|
119
|
+
// <svg width="100%" preserveAspectRatio="none"> inside a `.ML__stretchy`. That
|
|
120
|
+
// span is absolutely positioned, so its immediate wrapper computes to 0px and
|
|
121
|
+
// the width has to resolve against a *positioned* ancestor. Pin that to
|
|
122
|
+
// `.ML__base`, whose width already equals the accent's content (measured:
|
|
123
|
+
// 9.85px for a 0.56em slot), so the arc spans exactly its content.
|
|
124
|
+
//
|
|
125
|
+
// Do NOT write a bare `& svg` rule instead: MathLive's own rule is
|
|
126
|
+
// descendant-scoped (`.ML__latex .ML__stretchy svg`), and a global one
|
|
127
|
+
// re-parents every svg in the subtree.
|
|
128
|
+
'& .ML__base': {
|
|
129
|
+
position: 'relative',
|
|
130
|
+
},
|
|
131
|
+
'& .ML__stretchy': {
|
|
64
132
|
position: 'absolute',
|
|
133
|
+
left: 0,
|
|
65
134
|
width: '100%',
|
|
66
135
|
},
|
|
136
|
+
// MathLive centres an accent by shifting it right half the content width -
|
|
137
|
+
// `margin-left: 0.28em` for a 0.56em slot - which assumes a glyph with its own
|
|
138
|
+
// side bearings. Over a synthetic empty slot it just pushes the arc right.
|
|
139
|
+
//
|
|
140
|
+
// `!important` is required: MathLive writes this as an INLINE style
|
|
141
|
+
// (`style="top:-3.4em;margin-left:0.28em"`), which a class cannot override.
|
|
142
|
+
//
|
|
143
|
+
// It also fixes the width. `.ML__center` is the stretchy's containing block
|
|
144
|
+
// (MathLive gives `.ML__vlist > span` position:relative), and its width is the
|
|
145
|
+
// base minus that margin - measured 9.85px - 4.93px = 4.93px, exactly half,
|
|
146
|
+
// which is why the arc came out half-width AND offset right.
|
|
147
|
+
//
|
|
148
|
+
// Scoped with `:has(.ML__stretchy)`: only stretchy accents are affected.
|
|
149
|
+
// A single-glyph accent (`\hat{x}`, `\vec{x}`) genuinely needs its centring
|
|
150
|
+
// offset, and must keep it.
|
|
151
|
+
'& .ML__center:has(.ML__stretchy)': {
|
|
152
|
+
marginLeft: '0 !important',
|
|
153
|
+
},
|
|
67
154
|
};
|
|
68
155
|
|
|
69
156
|
/**
|
|
@@ -73,6 +160,7 @@ export const placeholderStyles = {
|
|
|
73
160
|
export const commonMqKeyboardStyles = {
|
|
74
161
|
...commonMqFontStyles,
|
|
75
162
|
...longdivStyles,
|
|
163
|
+
...rootIndexStyles,
|
|
76
164
|
...placeholderStyles,
|
|
77
165
|
touchAction: 'manipulation',
|
|
78
166
|
'& math-field': {
|
|
@@ -114,6 +202,7 @@ export const commonKeyboardStyles = commonMqKeyboardStyles;
|
|
|
114
202
|
export default {
|
|
115
203
|
commonMqFontStyles,
|
|
116
204
|
longdivStyles,
|
|
205
|
+
rootIndexStyles,
|
|
117
206
|
supsubStyles,
|
|
118
207
|
commonMqKeyboardStyles,
|
|
119
208
|
placeholderStyles,
|