@pie-lib/math-input-mathlive 1.1.3 → 1.1.5-beta.1
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/fonts/embedded.js +120 -0
- package/lib/fonts/index.js +81 -0
- package/lib/fonts/index.js.map +1 -0
- 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 +186 -15
- 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/scripts/generate-embedded-fonts.js +94 -0
- package/src/fonts/embedded.js +116 -0
- package/src/fonts/index.js +71 -0
- 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 +201 -14
- 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 -26
- 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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
|
|
3
|
+
import { EMBEDDED_FONTS, MATHLIVE_VERSION } from './embedded';
|
|
4
|
+
|
|
5
|
+
const log = debug('pie-lib:math-input-mathlive:fonts');
|
|
6
|
+
|
|
7
|
+
let registered = false;
|
|
8
|
+
|
|
9
|
+
/** The families MathLive needs; derived from the generated table. */
|
|
10
|
+
export const EMBEDDED_FONT_FAMILIES = Array.from(new Set(EMBEDDED_FONTS.map((f) => f.family)));
|
|
11
|
+
|
|
12
|
+
export { MATHLIVE_VERSION };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Put the embedded KaTeX faces into `document.fonts`.
|
|
16
|
+
*
|
|
17
|
+
* MathLive's `loadFonts()` short-circuits to "ready" as soon as every family it
|
|
18
|
+
* needs is already in `document.fonts`, so registering here means it never
|
|
19
|
+
* requests a woff2 over the network. That matters because MathLive composes some
|
|
20
|
+
* glyphs from Private Use Area codepoints that exist only in these fonts - `\ne`
|
|
21
|
+
* is U+E020 overlapped on `=` - so a blocked or missing font file does not
|
|
22
|
+
* degrade to a plainer glyph, it renders tofu. Hosts that ship JS-only bundles
|
|
23
|
+
* (pie-elements) have no stylesheet to carry `@font-face` and no asset path to
|
|
24
|
+
* serve, and a CSP that omits the CDN left exactly that broken.
|
|
25
|
+
*
|
|
26
|
+
* Skipped when the document already has every family - a host that serves the
|
|
27
|
+
* fonts itself, via `configureFonts` or by importing `mathlive/fonts.css`, wins.
|
|
28
|
+
*
|
|
29
|
+
* Idempotent, browser-only, and never throws: a font problem must not take the
|
|
30
|
+
* editor down.
|
|
31
|
+
*
|
|
32
|
+
* @returns {boolean} whether the faces are available in the document
|
|
33
|
+
*/
|
|
34
|
+
export const registerEmbeddedFonts = () => {
|
|
35
|
+
if (registered) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof document === 'undefined' || !document.fonts || typeof FontFace === 'undefined') {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const present = new Set(Array.from(document.fonts).map((f) => f.family));
|
|
45
|
+
|
|
46
|
+
if (EMBEDDED_FONT_FAMILIES.every((family) => present.has(family))) {
|
|
47
|
+
log('the document already provides every KaTeX family - leaving it alone');
|
|
48
|
+
registered = true;
|
|
49
|
+
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
EMBEDDED_FONTS.forEach(({ family, src, descriptors }) => {
|
|
54
|
+
document.fonts.add(new FontFace(family, src, descriptors));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
log('registered %d embedded KaTeX faces (mathlive@%s)', EMBEDDED_FONTS.length, MATHLIVE_VERSION);
|
|
58
|
+
registered = true;
|
|
59
|
+
|
|
60
|
+
return true;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
log('could not register the embedded fonts: %s', e && e.message);
|
|
63
|
+
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Test seam: forget that the faces were registered. */
|
|
69
|
+
export const resetEmbeddedFontsForTests = () => {
|
|
70
|
+
registered = false;
|
|
71
|
+
};
|
package/src/keypad/index.jsx
CHANGED
|
@@ -9,7 +9,7 @@ import { color } from '@pie-lib/render-ui';
|
|
|
9
9
|
|
|
10
10
|
import { baseSet } from '../keys';
|
|
11
11
|
import { sortKeys } from './keys-layout';
|
|
12
|
-
import { commonKeyboardStyles } from '../mf/common-styles';
|
|
12
|
+
import { commonKeyboardStyles, rootIndexStyles } from '../mf/common-styles';
|
|
13
13
|
import { loadMathLive, latexToMarkup } from '../mathlive-instance';
|
|
14
14
|
|
|
15
15
|
const log = debug('pie-lib:math-input-mathlive:keypad');
|
|
@@ -29,22 +29,20 @@ const LabelHolder = styled('span')(() => ({
|
|
|
29
29
|
pointerEvents: 'none',
|
|
30
30
|
textTransform: 'none',
|
|
31
31
|
color: color.text(),
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
// Deliberately inline-block, NOT inline-flex: MathLive's markup relies on
|
|
33
|
+
// normal inline flow. Making its spans flex items collapses their width, and
|
|
34
|
+
// stretchy accents render an <svg width="100%" preserveAspectRatio="none">
|
|
35
|
+
// that then squeezes into a spike instead of spanning the content.
|
|
36
|
+
display: 'inline-block',
|
|
37
|
+
textAlign: 'center',
|
|
35
38
|
lineHeight: 1,
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// the
|
|
41
|
-
// label's min-content contribution, so the grid can't blow out either way.
|
|
39
|
+
// Keep a label from sizing its grid column. `overflow: hidden` also collapses
|
|
40
|
+
// the label's min-content contribution, so a wide accent cannot drag the
|
|
41
|
+
// column across the screen. The svg's own positioning is left to
|
|
42
|
+
// mathlive-static.css (injected by the package) - duplicating it here broke
|
|
43
|
+
// the accent's containing block.
|
|
42
44
|
maxWidth: '100%',
|
|
43
45
|
overflow: 'hidden',
|
|
44
|
-
'& svg': {
|
|
45
|
-
position: 'absolute',
|
|
46
|
-
width: '100%',
|
|
47
|
-
},
|
|
48
46
|
// Answer-block / empty-slot boxes inside a label.
|
|
49
47
|
'& .ML__placeholder': {
|
|
50
48
|
backgroundColor: color.keypadEmptyPlaceholder ? color.keypadEmptyPlaceholder() : undefined,
|
|
@@ -55,6 +53,9 @@ const LabelHolder = styled('span')(() => ({
|
|
|
55
53
|
'& [data-pie-empty]': {
|
|
56
54
|
opacity: 0.5,
|
|
57
55
|
},
|
|
56
|
+
// A readable index on the nth-root key. Carried by the label itself, not only
|
|
57
|
+
// by the keypad container, so it holds wherever a LatexLabel is mounted.
|
|
58
|
+
...rootIndexStyles,
|
|
58
59
|
}));
|
|
59
60
|
|
|
60
61
|
export class LatexLabel extends React.Component {
|
package/src/keys/geometry.js
CHANGED
|
@@ -14,7 +14,10 @@ export const overRightArrow = set({
|
|
|
14
14
|
|
|
15
15
|
export const overLeftRightArrow = set({
|
|
16
16
|
name: 'Segment',
|
|
17
|
-
|
|
17
|
+
// MathQuill needed a nested \overline{} to draw the empty box; MathLive
|
|
18
|
+
// renders that as a real line, so the label showed a spurious bar under the
|
|
19
|
+
// arrows. `withVisibleEmptySlots` supplies the box for `{}` instead.
|
|
20
|
+
latex: '\\overleftrightarrow{}',
|
|
18
21
|
symbol: 'AB',
|
|
19
22
|
command: '\\overleftrightarrow',
|
|
20
23
|
});
|
|
@@ -51,7 +54,9 @@ export const angle = set({
|
|
|
51
54
|
});
|
|
52
55
|
export const overArc = set({
|
|
53
56
|
name: 'Over arc',
|
|
54
|
-
|
|
57
|
+
// See the note on Segment above: the nested \overline{} drew a straight line
|
|
58
|
+
// instead of leaving the arc visible.
|
|
59
|
+
latex: '\\overarc{}',
|
|
55
60
|
command: '\\overarc',
|
|
56
61
|
});
|
|
57
62
|
export const measureOfAngle = set({
|
package/src/keys/misc.js
CHANGED
|
@@ -29,10 +29,16 @@ export const brackets = set({
|
|
|
29
29
|
command: '[',
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
+
// `%` must be escaped. MathQuill was not a real LaTeX parser: it defined
|
|
33
|
+
// `LatexCmds['%']` as a symbol that *serialised* to `\%`, so a bare `%` worked
|
|
34
|
+
// both as a label and as a command. MathLive parses LaTeX properly, where `%`
|
|
35
|
+
// begins a comment - it rendered nothing (a blank button) and, once inserted,
|
|
36
|
+
// silently swallowed the rest of the expression. `\%` is what MathQuill stored
|
|
37
|
+
// all along, so this changes the key only, not the saved latex.
|
|
32
38
|
export const percentage = set({
|
|
33
39
|
name: 'Percent',
|
|
34
|
-
latex: '
|
|
35
|
-
command: '
|
|
40
|
+
latex: '\\%',
|
|
41
|
+
command: '\\%',
|
|
36
42
|
});
|
|
37
43
|
|
|
38
44
|
export const approx = set({
|
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,7 @@
|
|
|
1
1
|
import debug from 'debug';
|
|
2
2
|
import { NEWLINE_EMBED, toMathLive, withVisibleEmptySlots } from './latex-bridge';
|
|
3
|
+
import { registerEmbeddedFonts } from './fonts';
|
|
4
|
+
import { ROOT_INDEX_CSS } from './mf/common-styles';
|
|
3
5
|
|
|
4
6
|
const log = debug('pie-lib:math-input-mathlive:instance');
|
|
5
7
|
|
|
@@ -46,26 +48,66 @@ const absoluteFontsDirectory = (dir) => {
|
|
|
46
48
|
}
|
|
47
49
|
};
|
|
48
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Last-resort CDN for the KaTeX fonts.
|
|
53
|
+
*
|
|
54
|
+
* Some symbols are composed from Private Use Area glyphs that exist only in
|
|
55
|
+
* those fonts: `\\neq` is U+E020 (a negation slash) overlapped on `=`, and
|
|
56
|
+
* `\\nsim` / `\\ncong` work the same way. With no fonts the browser substitutes
|
|
57
|
+
* an arbitrary glyph for U+E020, so those keys render as garbage rather than
|
|
58
|
+
* simply falling back to a plainer face.
|
|
59
|
+
*
|
|
60
|
+
* The fonts normally come from inside this package (see `./fonts`), so this URL
|
|
61
|
+
* is only reached on a browser without the FontFace API, where MathLive has to
|
|
62
|
+
* fetch the woff2 files itself. The version comes from MathLive, so the fonts
|
|
63
|
+
* always match the engine.
|
|
64
|
+
*/
|
|
65
|
+
const FALLBACK_MATHLIVE_VERSION = '0.110.0';
|
|
66
|
+
|
|
67
|
+
const cdnFontsDirectory = () => {
|
|
68
|
+
const version = (mathlive && mathlive.version && mathlive.version.mathlive) || FALLBACK_MATHLIVE_VERSION;
|
|
69
|
+
|
|
70
|
+
// NOTE: `/fonts`, not `/dist/fonts` - the published package root holds them.
|
|
71
|
+
return `https://unpkg.com/mathlive@${version}/fonts`;
|
|
72
|
+
};
|
|
73
|
+
|
|
49
74
|
/**
|
|
50
75
|
* Tell the package where the MathLive fonts are served from.
|
|
51
76
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
77
|
+
* Optional: by default the fonts come from inside this package (see `./fonts`),
|
|
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.
|
|
55
81
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
82
|
+
* - a path/URL: served by you; the embedded fonts are not registered
|
|
83
|
+
* - `null`: skip font loading entirely; note `\\neq`, `\\nsim` and `\\ncong`
|
|
84
|
+
* then render incorrectly, being built from Private Use Area
|
|
85
|
+
* glyphs that only these fonts have
|
|
86
|
+
* - not called: the embedded fonts, with the CDN as a fallback for browsers
|
|
87
|
+
* without the FontFace API
|
|
58
88
|
*
|
|
59
|
-
* @param {string|null} dir
|
|
89
|
+
* @param {string|null} [dir]
|
|
60
90
|
*/
|
|
61
91
|
export const configureFonts = (dir) => {
|
|
62
92
|
fontsDirectory = dir;
|
|
63
93
|
|
|
64
94
|
if (mathlive) {
|
|
65
|
-
mathlive.MathfieldElement.fontsDirectory = absoluteFontsDirectory(dir);
|
|
95
|
+
mathlive.MathfieldElement.fontsDirectory = dir === undefined ? cdnFontsDirectory() : absoluteFontsDirectory(dir);
|
|
66
96
|
}
|
|
67
97
|
};
|
|
68
98
|
|
|
99
|
+
/**
|
|
100
|
+
* What the spacebar inserts in math mode.
|
|
101
|
+
*
|
|
102
|
+
* MathLive's `mathModeSpace` defaults to an empty string, which makes the
|
|
103
|
+
* spacebar a no-op - users cannot type a space at all. `\\ ` (a LaTeX control
|
|
104
|
+
* space) is what MathQuill serialised spaces as, so existing content and the
|
|
105
|
+
* MathJax renderer in @pie-lib/math-rendering both already understand it.
|
|
106
|
+
*
|
|
107
|
+
* A literal " " is not usable: LaTeX skips raw whitespace.
|
|
108
|
+
*/
|
|
109
|
+
export const MATH_MODE_SPACE = '\\ ';
|
|
110
|
+
|
|
69
111
|
/**
|
|
70
112
|
* Custom macros for the pie-specific commands that the MathQuill fork
|
|
71
113
|
* understood but stock LaTeX does not.
|
|
@@ -125,22 +167,167 @@ let loadWarned = false;
|
|
|
125
167
|
|
|
126
168
|
/** Apply post-load configuration exactly once. */
|
|
127
169
|
const configureLoaded = () => {
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
170
|
+
// No host path configured: use the fonts that ship inside this package. Once
|
|
171
|
+
// they are in `document.fonts`, MathLive's own loader short-circuits and never
|
|
172
|
+
// requests a woff2 - so math renders offline and under a CSP that allows no
|
|
173
|
+
// font origin at all.
|
|
174
|
+
if (fontsDirectory === undefined && !registerEmbeddedFonts()) {
|
|
132
175
|
log(
|
|
133
|
-
'no
|
|
134
|
-
'
|
|
176
|
+
'embedded fonts unavailable (no FontFace API) - falling back to the CDN (%s). ' +
|
|
177
|
+
'Serve the fonts yourself and call configureFonts("/path/to/fonts") to avoid the network request.',
|
|
178
|
+
cdnFontsDirectory(),
|
|
135
179
|
);
|
|
136
180
|
}
|
|
137
181
|
|
|
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.
|
|
138
186
|
mathlive.MathfieldElement.fontsDirectory =
|
|
139
|
-
fontsDirectory === undefined ?
|
|
187
|
+
fontsDirectory === undefined ? cdnFontsDirectory() : absoluteFontsDirectory(fontsDirectory);
|
|
140
188
|
|
|
141
189
|
injectCoreStylesheet();
|
|
142
190
|
};
|
|
143
191
|
|
|
192
|
+
/**
|
|
193
|
+
* CSS that has to live *inside* a mathfield's shadow root.
|
|
194
|
+
*
|
|
195
|
+
* A live `<math-field>` renders in shadow DOM (`attachShadow({mode:'open'})`),
|
|
196
|
+
* so nothing in the host page's stylesheets reaches it. The stretchy-accent
|
|
197
|
+
* problem therefore needs fixing twice: once for static markup (see
|
|
198
|
+
* `placeholderStyles`) and once here for the editor.
|
|
199
|
+
*
|
|
200
|
+
* MathLive centres an accent with `margin-left: <half the content width>` as an
|
|
201
|
+
* inline style. For a stretchy accent that also shrinks its containing block
|
|
202
|
+
* (`.ML__center` is position:relative), so the arc ends up half-width and offset
|
|
203
|
+
* - visibly failing to span the text underneath.
|
|
204
|
+
*
|
|
205
|
+
* `:has(.ML__stretchy)` keeps the fix to stretchy accents; single-glyph accents
|
|
206
|
+
* (`\hat{x}`, `\vec{x}`) still need their centring offset.
|
|
207
|
+
*/
|
|
208
|
+
/**
|
|
209
|
+
* Marker put on the host `<math-field>` while a `\placeholder{}` is selected,
|
|
210
|
+
* so the caret rules below apply to a placeholder and nothing else.
|
|
211
|
+
*/
|
|
212
|
+
export const PLACEHOLDER_CARET_CLASS = 'pie-caret-in-placeholder';
|
|
213
|
+
|
|
214
|
+
/** What MathLive serialises a `#?` slot as. */
|
|
215
|
+
const PLACEHOLDER_LATEX = '\\placeholder{}';
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* A blinking caret inside a selected placeholder box.
|
|
219
|
+
*
|
|
220
|
+
* A keypad key such as `\frac` inserts `\frac{#?}{#?}`. MathLive turns each
|
|
221
|
+
* `#?` into a `\placeholder{}` atom and *selects* the first one. That atom is a
|
|
222
|
+
* leaf - the offsets around it are "before" and "after", there is no position
|
|
223
|
+
* inside it - so MathLive renders **no caret element at all** and shows only
|
|
224
|
+
* the selection highlight. The box therefore looked focused (blue) but dead,
|
|
225
|
+
* where MathQuill used to blink a cursor inside its empty box.
|
|
226
|
+
*
|
|
227
|
+
* So the caret is drawn here. `:not(:has(.ML__selected))` picks the innermost
|
|
228
|
+
* selected element (MathLive puts the class on a wrapper *and* the glyph span,
|
|
229
|
+
* which would otherwise give two carets), and the `ML__caret-blink` keyframes
|
|
230
|
+
* plus `--_caret-color` are MathLive's own, already in this shadow root.
|
|
231
|
+
*
|
|
232
|
+
* Typing still replaces the whole placeholder, which for an empty box is
|
|
233
|
+
* indistinguishable from inserting at the caret.
|
|
234
|
+
*/
|
|
235
|
+
const PLACEHOLDER_CARET_CSS = [
|
|
236
|
+
`:host(.${PLACEHOLDER_CARET_CLASS}) .ML__selected:not(:has(.ML__selected)){position:relative}`,
|
|
237
|
+
`:host(.${PLACEHOLDER_CARET_CLASS}) .ML__selected:not(:has(.ML__selected))::after{`,
|
|
238
|
+
'content:"";position:absolute;left:50%;top:12%;height:76%;',
|
|
239
|
+
'border-right:2px solid var(--_caret-color, currentColor);',
|
|
240
|
+
'animation:ML__caret-blink 1.05s step-end forwards infinite}',
|
|
241
|
+
].join('');
|
|
242
|
+
|
|
243
|
+
// A live mathfield needs the readable radical index too: `rootIndexStyles` is
|
|
244
|
+
// written for static markup in the host page, which cannot cross the shadow
|
|
245
|
+
// boundary.
|
|
246
|
+
const SHADOW_CSS =
|
|
247
|
+
'.ML__latex .ML__center:has(.ML__stretchy){margin-left:0 !important}' + ROOT_INDEX_CSS + PLACEHOLDER_CARET_CSS;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Keep {@link PLACEHOLDER_CARET_CLASS} in step with the selection.
|
|
251
|
+
*
|
|
252
|
+
* Gating on the class is what keeps the caret specific to placeholders: an
|
|
253
|
+
* ordinary selection (`x+1`) must not sprout one. MathLive fires
|
|
254
|
+
* `selection-change` for every caret move, including the one that lands on the
|
|
255
|
+
* placeholder straight after an insert.
|
|
256
|
+
*
|
|
257
|
+
* @param {HTMLElement} mathField
|
|
258
|
+
* @returns {function|undefined} teardown, to call on unmount
|
|
259
|
+
*/
|
|
260
|
+
export const trackPlaceholderCaret = (mathField) => {
|
|
261
|
+
if (!mathField || typeof mathField.addEventListener !== 'function') {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const update = () => {
|
|
266
|
+
let selected = '';
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
// A collapsed selection already has a real MathLive caret.
|
|
270
|
+
if (!mathField.selectionIsCollapsed && mathField.selection) {
|
|
271
|
+
selected = mathField.getValue(mathField.selection, 'latex');
|
|
272
|
+
}
|
|
273
|
+
} catch (e) {
|
|
274
|
+
log('could not read the selection: %s', e && e.message);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (mathField.classList) {
|
|
278
|
+
mathField.classList.toggle(PLACEHOLDER_CARET_CLASS, selected === PLACEHOLDER_LATEX);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
mathField.addEventListener('selection-change', update);
|
|
283
|
+
update();
|
|
284
|
+
|
|
285
|
+
return () => mathField.removeEventListener('selection-change', update);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Apply {@link SHADOW_CSS} inside a mathfield's shadow root.
|
|
290
|
+
*
|
|
291
|
+
* Uses `adoptedStyleSheets` when available (which is how MathLive loads its own
|
|
292
|
+
* stylesheets) and falls back to appending a <style> node.
|
|
293
|
+
*
|
|
294
|
+
* @param {HTMLElement} mathField
|
|
295
|
+
*/
|
|
296
|
+
export const applyShadowStyles = (mathField) => {
|
|
297
|
+
const root = mathField && mathField.shadowRoot;
|
|
298
|
+
|
|
299
|
+
if (!root) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
if ('adoptedStyleSheets' in root && typeof CSSStyleSheet !== 'undefined') {
|
|
305
|
+
// Do not re-add on a re-render.
|
|
306
|
+
if (root.__pieAccentSheet) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const sheet = new CSSStyleSheet();
|
|
311
|
+
|
|
312
|
+
sheet.replaceSync(SHADOW_CSS);
|
|
313
|
+
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
|
|
314
|
+
root.__pieAccentSheet = sheet;
|
|
315
|
+
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (!root.querySelector('style[data-pie-accent]')) {
|
|
320
|
+
const style = document.createElement('style');
|
|
321
|
+
|
|
322
|
+
style.setAttribute('data-pie-accent', '');
|
|
323
|
+
style.textContent = SHADOW_CSS;
|
|
324
|
+
root.appendChild(style);
|
|
325
|
+
}
|
|
326
|
+
} catch (e) {
|
|
327
|
+
log('could not apply shadow styles to the mathfield: %s', e && e.message);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
144
331
|
/**
|
|
145
332
|
* Ensure MathLive's "core" stylesheet is in the document.
|
|
146
333
|
*
|
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
|
};
|