@pie-lib/math-input-mathlive 1.1.5-beta.11 → 1.1.5-beta.13

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.
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Every place that draws MathLive output must apply the long-division
3
+ * correction (see longdiv.js): live fields after every render, static markup
4
+ * after it is written into the DOM.
5
+ */
6
+ jest.mock('../mathlive-instance', () => {
7
+ class FakeMathfieldElement {
8
+ constructor() {
9
+ const el = global.document.createElement('math-field');
10
+
11
+ el.value = '';
12
+ el.getValue = () => el.value;
13
+
14
+ return el;
15
+ }
16
+ }
17
+
18
+ return {
19
+ loadMathLive: () => Promise.resolve({ MathfieldElement: FakeMathfieldElement }),
20
+ getMacros: () => ({}),
21
+ MATH_MODE_SPACE: '\\ ',
22
+ applyShadowStyles: () => {},
23
+ trackPlaceholderCaret: () => () => {},
24
+ latexToMarkup: (l) => `<span>${l}</span>`,
25
+ };
26
+ });
27
+
28
+ const mockUntrack = jest.fn();
29
+
30
+ jest.mock('../longdiv', () => ({
31
+ trackLongDivision: jest.fn(() => mockUntrack),
32
+ fixLongDivisionWhenReady: jest.fn(),
33
+ fixLongDivision: jest.fn(),
34
+ }));
35
+
36
+ /* eslint-disable import/first */
37
+ import Input from '../mf/input';
38
+ import Static from '../mf/static';
39
+ import { LatexLabel } from '../keypad';
40
+ import { trackLongDivision, fixLongDivisionWhenReady } from '../longdiv';
41
+ /* eslint-enable import/first */
42
+
43
+ beforeEach(() => {
44
+ trackLongDivision.mockClear();
45
+ fixLongDivisionWhenReady.mockClear();
46
+ mockUntrack.mockClear();
47
+ });
48
+
49
+ describe('Input (the editor)', () => {
50
+ it('tracks the mathfield on mount and stops on unmount', async () => {
51
+ const input = new Input({});
52
+
53
+ input.holderRef = { current: document.createElement('span') };
54
+ await input.componentDidMount();
55
+
56
+ expect(trackLongDivision).toHaveBeenCalledWith(input.mathField);
57
+
58
+ input.componentWillUnmount();
59
+
60
+ expect(mockUntrack).toHaveBeenCalledTimes(1);
61
+ expect(input.untrackLongDivision).toBeUndefined();
62
+ });
63
+ });
64
+
65
+ describe('Static', () => {
66
+ it('tracks the answer-block field on mount and stops on unmount', async () => {
67
+ const s = new Static({ latex: 'x=\\MathQuillMathField[r1]{}' });
68
+
69
+ s.holderRef = { current: document.createElement('span') };
70
+ s.createLiveRegion = () => {};
71
+ await s.componentDidMount();
72
+
73
+ expect(trackLongDivision).toHaveBeenCalledWith(s.mathField);
74
+
75
+ s.componentWillUnmount();
76
+
77
+ expect(mockUntrack).toHaveBeenCalledTimes(1);
78
+ });
79
+
80
+ // Display mode: the markup reaches the DOM through state, so it is corrected
81
+ // once React has written it.
82
+ it('corrects static markup after it is rendered', () => {
83
+ const s = new Static({ latex: '5\\longdiv{144}' });
84
+ const holder = document.createElement('span');
85
+
86
+ s.holderRef = { current: holder };
87
+ s.state = { markup: '<span>new</span>' };
88
+ s.componentDidUpdate({ latex: '5\\longdiv{144}' }, { markup: '' });
89
+
90
+ expect(fixLongDivisionWhenReady).toHaveBeenCalledWith(holder);
91
+ });
92
+
93
+ it('does not redo it when the markup did not change', () => {
94
+ const s = new Static({ latex: 'x' });
95
+
96
+ s.holderRef = { current: document.createElement('span') };
97
+ s.state = { markup: '<span>same</span>' };
98
+ s.componentDidUpdate({ latex: 'x' }, { markup: '<span>same</span>' });
99
+
100
+ expect(fixLongDivisionWhenReady).not.toHaveBeenCalled();
101
+ });
102
+
103
+ it('leaves a live field to the tracker', () => {
104
+ const s = new Static({ latex: 'x' });
105
+
106
+ s.holderRef = { current: document.createElement('span') };
107
+ s.mathField = document.createElement('math-field');
108
+ s.state = { markup: '<span>new</span>' };
109
+ s.componentDidUpdate({ latex: 'x' }, { markup: '' });
110
+
111
+ expect(fixLongDivisionWhenReady).not.toHaveBeenCalled();
112
+ });
113
+ });
114
+
115
+ describe('keypad label', () => {
116
+ it('corrects the label once mounted', async () => {
117
+ const label = new LatexLabel({ latex: '\\longdiv{}' });
118
+ const el = document.createElement('span');
119
+
120
+ label.labelRef = { current: el };
121
+ await label.componentDidMount();
122
+
123
+ expect(fixLongDivisionWhenReady).toHaveBeenCalledWith(el);
124
+ });
125
+
126
+ it('corrects it again when the label changes', () => {
127
+ const label = new LatexLabel({ latex: '\\longdiv{}' });
128
+ const el = document.createElement('span');
129
+
130
+ label.labelRef = { current: el };
131
+ label.setState = () => {};
132
+ label.state = { markup: '<span>other</span>' };
133
+ label.componentDidUpdate({ latex: '\\longdiv{}' }, { markup: '<span>first</span>' });
134
+
135
+ expect(fixLongDivisionWhenReady).toHaveBeenCalledWith(el);
136
+ });
137
+ });
@@ -0,0 +1,259 @@
1
+ import { fixLongDivision, fixLongDivisionWhenReady, trackLongDivision } from '../longdiv';
2
+
3
+ /**
4
+ * Markup captured verbatim from MathLive 0.110.0 `convertLatexToMarkup` in
5
+ * Chrome. In `12\enclose{longdiv}{144}` MathLive drops the dividend 0.45em
6
+ * (`top:0.45em`), sizes the box 2.1em wide while the viewBox is 2.54 wide, and
7
+ * ends the bar at 1.8 although the dividend runs to 0.3 + 0.45 + 1.5 = 2.25.
8
+ */
9
+ const LONGDIV =
10
+ '<span class="ML__latex"><span class="ML__strut" style="height:0.8em"></span><span class="ML__strut--bottom" style="height:1.25em;vertical-align:-0.44em"></span><span class="ML__base"><span class="ML__cmr">12</span><span style="position:relative;height:1.24em;display:inline-block;margin-left:0.3em;margin-right:0.3em;width:1.95em"><span class="ML__notation" style="box-sizing:border-box;left:calc(-1 * 1px - 0.3em);height:calc( 1.24444em + 1px);top:0em;width:calc(2.1em + 1px);background-color:transparent"><span style="display: inline-block;height:1.24em;vertical-align:0.44em;"></span><svg style="position:absolute;overflow:visible;height:1.24em;top:0;left:0;width:100%;z-index:2;" stroke-width="0.06em" stroke="currentColor" stroke-linecap="round" viewBox="0 0 2.54 1.24"><line x1="0.3" y1="0.3" x2="1.8" y2="0.3" vector-effect="non-scaling-stroke"></line><path d="M 0.3 0.3 a0.3 0.46997, 0, 1, 1, 0 0.93994 " stroke-width="0.004em" stroke="currentColor" fill="none"/></svg></span><span style="position:relative;display:inline-block;height:0.65em;width:1.5em;margin-left:0.45em;top:0.45em"><span class="ML__cmr">144</span></span></span></span></span>';
11
+
12
+ // \enclose{box}{x}: another notation, which must be left alone
13
+ const BOX =
14
+ '<span class="ML__latex"><span class="ML__strut" style="height:1.04em"></span><span class="ML__base"><span style="position:relative;height:1.03em;display:inline-block;margin-left:0.3em;margin-right:0.3em;width:0.58em"><span class="ML__notation" style="box-sizing:border-box;left:calc(-1 * 1px - 0.3em);height:calc( 1.03056em + 1px);top:0em;width:calc(1.17153em + 1px);background-color:transparent;border:1px solid red"></span><span style="position:relative;display:inline-block;height:0.44em;width:0.58em"><span class="ML__mathit">x</span></span></span></span></span>';
15
+
16
+ const mount = (html) => {
17
+ const el = document.createElement('div');
18
+
19
+ el.innerHTML = html;
20
+ document.body.appendChild(el);
21
+
22
+ return el;
23
+ };
24
+
25
+ const parts = (el) => {
26
+ const notation = el.querySelector('.ML__notation');
27
+
28
+ return {
29
+ notation,
30
+ box: notation.parentElement,
31
+ dividend: notation.nextElementSibling,
32
+ line: notation.querySelector('line'),
33
+ };
34
+ };
35
+
36
+ afterEach(() => {
37
+ document.body.innerHTML = '';
38
+ });
39
+
40
+ describe('fixLongDivision', () => {
41
+ it('puts the dividend back on the baseline', () => {
42
+ const el = mount(LONGDIV);
43
+
44
+ fixLongDivision(el);
45
+
46
+ const { dividend, box } = parts(el);
47
+
48
+ expect(dividend.style.top).toEqual('0px');
49
+ // the drawing keeps its place relative to the dividend, and the line grows
50
+ // so the field does not clip the bar
51
+ expect(box.style.paddingTop).toEqual('0.45em');
52
+ });
53
+
54
+ it('sizes the box to the viewBox, so the drawing is not scaled down', () => {
55
+ const el = mount(LONGDIV);
56
+
57
+ fixLongDivision(el);
58
+
59
+ expect(parts(el).notation.style.width).toEqual('calc(2.54em + 1px)');
60
+ });
61
+
62
+ it('runs the bar to the end of the dividend', () => {
63
+ const el = mount(LONGDIV);
64
+
65
+ fixLongDivision(el);
66
+
67
+ // no layout under jsdom, so from MathLive's numbers: start + gap + width
68
+ expect(parseFloat(parts(el).line.getAttribute('x2'))).toBeCloseTo(0.3 + 0.45 + 1.5, 6);
69
+ });
70
+
71
+ // MathLive's width leaves out the spacing around `,` and `+`, so when the
72
+ // markup is laid out the end is measured instead.
73
+ it('measures the bar end from the rendered dividend when laid out', () => {
74
+ const el = mount(LONGDIV);
75
+ const { notation, dividend } = parts(el);
76
+ const svg = notation.querySelector('svg');
77
+ const glyph = dividend.querySelector('.ML__cmr');
78
+
79
+ // the svg is 254px wide for a 2.54-unit viewBox: 100px per unit
80
+ svg.getBoundingClientRect = () => ({ left: 100, width: 254, right: 354 });
81
+ glyph.getBoundingClientRect = () => ({ left: 145, right: 340, width: 195 });
82
+
83
+ fixLongDivision(el);
84
+
85
+ expect(parseFloat(parts(el).line.getAttribute('x2'))).toBeCloseTo(2.4, 6);
86
+ });
87
+
88
+ it('ignores the caret when measuring', () => {
89
+ const el = mount(LONGDIV);
90
+ const { notation, dividend } = parts(el);
91
+ const caret = document.createElement('span');
92
+
93
+ caret.className = 'ML__caret';
94
+ dividend.appendChild(caret);
95
+ notation.querySelector('svg').getBoundingClientRect = () => ({ left: 100, width: 254, right: 354 });
96
+ dividend.querySelector('.ML__cmr').getBoundingClientRect = () => ({ left: 145, right: 340, width: 195 });
97
+ caret.getBoundingClientRect = () => ({ left: 400, right: 402, width: 2 });
98
+
99
+ fixLongDivision(el);
100
+
101
+ expect(parseFloat(parts(el).line.getAttribute('x2'))).toBeCloseTo(2.4, 6);
102
+ });
103
+
104
+ it('can run again without drifting', () => {
105
+ const el = mount(LONGDIV);
106
+
107
+ fixLongDivision(el);
108
+ const first = { ...parts(el), x2: parts(el).line.getAttribute('x2') };
109
+
110
+ fixLongDivision(el);
111
+ const second = parts(el);
112
+
113
+ expect(second.line.getAttribute('x2')).toEqual(first.x2);
114
+ expect(second.box.style.paddingTop).toEqual('0.45em');
115
+ expect(second.notation.style.width).toEqual('calc(2.54em + 1px)');
116
+ });
117
+
118
+ it('leaves other enclose notations alone', () => {
119
+ const el = mount(BOX);
120
+ const before = el.innerHTML;
121
+
122
+ expect(fixLongDivision(el)).toBe(0);
123
+ expect(el.innerHTML).toEqual(before);
124
+ });
125
+
126
+ it('counts what it corrected', () => {
127
+ expect(fixLongDivision(mount(LONGDIV + LONGDIV))).toBe(2);
128
+ });
129
+
130
+ it('is safe on nothing', () => {
131
+ expect(fixLongDivision(null)).toBe(0);
132
+ expect(fixLongDivision(mount('<span>plain</span>'))).toBe(0);
133
+ });
134
+ });
135
+
136
+ describe('fixLongDivisionWhenReady', () => {
137
+ it('corrects now, and again once the fonts have loaded', async () => {
138
+ let fontsLoaded;
139
+ const original = document.fonts;
140
+
141
+ Object.defineProperty(document, 'fonts', {
142
+ configurable: true,
143
+ value: { ready: new Promise((resolve) => (fontsLoaded = resolve)) },
144
+ });
145
+
146
+ try {
147
+ const el = mount(LONGDIV);
148
+
149
+ fixLongDivisionWhenReady(el);
150
+ expect(parts(el).dividend.style.top).toEqual('0px');
151
+
152
+ // a re-render before the fonts arrive...
153
+ el.innerHTML = LONGDIV;
154
+ fontsLoaded();
155
+ await Promise.resolve();
156
+ await Promise.resolve();
157
+
158
+ // ...is corrected once they do
159
+ expect(parts(el).dividend.style.top).toEqual('0px');
160
+ } finally {
161
+ Object.defineProperty(document, 'fonts', { configurable: true, value: original });
162
+ }
163
+ });
164
+ });
165
+
166
+ describe('trackLongDivision', () => {
167
+ const field = () => {
168
+ const host = document.createElement('div');
169
+
170
+ host.attachShadow({ mode: 'open' });
171
+ host.shadowRoot.innerHTML = LONGDIV;
172
+ document.body.appendChild(host);
173
+
174
+ return host;
175
+ };
176
+
177
+ it('corrects the field straight away', () => {
178
+ const host = field();
179
+
180
+ trackLongDivision(host);
181
+
182
+ expect(parts(host.shadowRoot).dividend.style.top).toEqual('0px');
183
+ });
184
+
185
+ // jest.setup.js replaces MutationObserver with a no-op for every test, so this
186
+ // installs one that records what it was asked to observe and can be fired.
187
+ describe('re-rendering', () => {
188
+ let observers;
189
+ const RealObserver = global.MutationObserver;
190
+
191
+ beforeEach(() => {
192
+ observers = [];
193
+ global.MutationObserver = class {
194
+ constructor(callback) {
195
+ this.callback = callback;
196
+ this.disconnected = false;
197
+ observers.push(this);
198
+ }
199
+
200
+ observe(target, options) {
201
+ this.target = target;
202
+ this.options = options;
203
+ }
204
+
205
+ disconnect() {
206
+ this.disconnected = true;
207
+ }
208
+ };
209
+ });
210
+
211
+ afterEach(() => {
212
+ global.MutationObserver = RealObserver;
213
+ });
214
+
215
+ it('watches the whole shadow tree for MathLive rebuilding it', () => {
216
+ const host = field();
217
+
218
+ trackLongDivision(host);
219
+
220
+ expect(observers).toHaveLength(1);
221
+ expect(observers[0].target).toBe(host.shadowRoot);
222
+ expect(observers[0].options).toEqual({ childList: true, subtree: true });
223
+ });
224
+
225
+ // MathLive rebuilds its shadow DOM on every keystroke, resize and font load.
226
+ it('re-corrects after MathLive re-renders', () => {
227
+ const host = field();
228
+
229
+ trackLongDivision(host);
230
+ host.shadowRoot.innerHTML = LONGDIV;
231
+ expect(parts(host.shadowRoot).dividend.style.top).toEqual('0.45em');
232
+
233
+ observers[0].callback([]);
234
+
235
+ expect(parts(host.shadowRoot).dividend.style.top).toEqual('0px');
236
+ });
237
+
238
+ // The corrections change attributes only, so observing childList alone
239
+ // cannot loop.
240
+ it('does not observe attributes', () => {
241
+ trackLongDivision(field());
242
+
243
+ expect(observers[0].options.attributes).toBeUndefined();
244
+ });
245
+
246
+ it('stops after teardown', () => {
247
+ const untrack = trackLongDivision(field());
248
+
249
+ untrack();
250
+
251
+ expect(observers[0].disconnected).toBe(true);
252
+ });
253
+ });
254
+
255
+ it('is a no-op without a shadow root', () => {
256
+ expect(trackLongDivision(undefined)).toBeUndefined();
257
+ expect(trackLongDivision(document.createElement('div'))).toBeUndefined();
258
+ });
259
+ });
@@ -8,17 +8,52 @@ const table = (rows) => (i) => (rows[i] ? { depth: rows[i][0], latex: rows[i][1]
8
8
 
9
9
  // 5x^2+2x+\frac{\pi}{2}-4
10
10
  const POLY = table([
11
- [0, ''], [0, '5'], [0, 'x'], [1, ''], [1, '2'], [0, '^2'], [0, '+'], [0, '2'], [0, 'x'], [0, '+'],
12
- [1, ''], [1, '\\pi'], [1, ''], [1, '2'], [0, '\\frac{\\pi}{2}'], [0, '-'], [0, '4'],
11
+ [0, ''],
12
+ [0, '5'],
13
+ [0, 'x'],
14
+ [1, ''],
15
+ [1, '2'],
16
+ [0, '^2'],
17
+ [0, '+'],
18
+ [0, '2'],
19
+ [0, 'x'],
20
+ [0, '+'],
21
+ [1, ''],
22
+ [1, '\\pi'],
23
+ [1, ''],
24
+ [1, '2'],
25
+ [0, '\\frac{\\pi}{2}'],
26
+ [0, '-'],
27
+ [0, '4'],
13
28
  ]);
14
29
  const POLY_LAST = 16;
15
30
 
16
31
  // a+\sqrt{x+1}+b
17
- const ROOT = table([[0, ''], [0, 'a'], [0, '+'], [1, ''], [1, 'x'], [1, '+'], [1, '1'], [0, '\\sqrt{x+1}'], [0, '+'], [0, 'b']]);
32
+ const ROOT = table([
33
+ [0, ''],
34
+ [0, 'a'],
35
+ [0, '+'],
36
+ [1, ''],
37
+ [1, 'x'],
38
+ [1, '+'],
39
+ [1, '1'],
40
+ [0, '\\sqrt{x+1}'],
41
+ [0, '+'],
42
+ [0, 'b'],
43
+ ]);
18
44
 
19
45
  // \frac{\frac{a}{b}}{c}
20
46
  const NESTED = table([
21
- [0, ''], [1, ''], [2, ''], [2, 'a'], [2, ''], [2, 'b'], [1, '\\frac{a}{b}'], [1, ''], [1, 'c'], [0, '\\frac{\\frac{a}{b}}{c}'],
47
+ [0, ''],
48
+ [1, ''],
49
+ [2, ''],
50
+ [2, 'a'],
51
+ [2, ''],
52
+ [2, 'b'],
53
+ [1, '\\frac{a}{b}'],
54
+ [1, ''],
55
+ [1, 'c'],
56
+ [0, '\\frac{\\frac{a}{b}}{c}'],
22
57
  ]);
23
58
 
24
59
  describe('expandRange', () => {
@@ -11,6 +11,7 @@ import { baseSet } from '../keys';
11
11
  import { sortKeys } from './keys-layout';
12
12
  import { commonKeyboardStyles, rootIndexStyles } from '../mf/common-styles';
13
13
  import { loadMathLive, latexToMarkup } from '../mathlive-instance';
14
+ import { fixLongDivisionWhenReady } from '../longdiv';
14
15
 
15
16
  const log = debug('pie-lib:math-input-mathlive:keypad');
16
17
 
@@ -68,6 +69,28 @@ const LabelHolder = styled('span', { shouldForwardProp: (prop) => prop !== 'slan
68
69
  '& [data-pie-empty]': {
69
70
  opacity: 0.5,
70
71
  },
72
+ // A fraction is taller than the label's 1em line box, so its numerator sat
73
+ // flush against (and was clipped by) the top edge. Extra room above it grows
74
+ // the box, which the keypad then centres, so the fraction moves down a little.
75
+ '&:has(.ML__mfrac)': {
76
+ paddingTop: '0.25em',
77
+ },
78
+ // Centre a fraction's bar between its empty slots (see `placeholderStyles`).
79
+ '& .ML__mfrac:has([data-pie-empty]) .ML__frac-line': {
80
+ position: 'relative',
81
+ top: '0.16em',
82
+ },
83
+ // A real numerator (`x`) is lighter than an empty box and its ink ends at the
84
+ // baseline, so the full drop overshoots: it needs a smaller drop. The numerator is
85
+ // the second `.ML__center` (MathLive emits the denominator first).
86
+ '& .ML__mfrac:has([data-pie-empty]):not(:has(.ML__center ~ .ML__center [data-pie-empty])) .ML__frac-line': {
87
+ top: '0.13em',
88
+ },
89
+ // Same for a mixed number (`x` then the fraction): the leading `x` is a sibling
90
+ // of the fraction, which a lone `\frac{}{}` does not have.
91
+ '& .ML__mfrac:has([data-pie-empty]):not(:only-child) .ML__frac-line': {
92
+ top: '0.13em',
93
+ },
71
94
  // A readable index on the nth-root key. Carried by the label itself, not only
72
95
  // by the keypad container, so it holds wherever a LatexLabel is mounted.
73
96
  ...rootIndexStyles,
@@ -81,11 +104,14 @@ export class LatexLabel extends React.Component {
81
104
  constructor(props) {
82
105
  super(props);
83
106
  this.state = { markup: latexToMarkup(props.latex) };
107
+ this.labelRef = React.createRef();
84
108
  }
85
109
 
86
110
  async componentDidMount() {
87
111
  // MathLive may not have finished loading when the keypad first mounts.
88
112
  if (this.state.markup) {
113
+ // The long-division key's label needs MathLive's drawing corrected.
114
+ fixLongDivisionWhenReady(this.labelRef.current);
89
115
  return;
90
116
  }
91
117
 
@@ -111,10 +137,14 @@ export class LatexLabel extends React.Component {
111
137
  }
112
138
  }
113
139
 
114
- componentDidUpdate(prevProps) {
140
+ componentDidUpdate(prevProps, prevState) {
115
141
  if (prevProps.latex !== this.props.latex) {
116
142
  this.setState({ markup: latexToMarkup(this.props.latex) });
117
143
  }
144
+
145
+ if (this.state.markup !== (prevState && prevState.markup)) {
146
+ fixLongDivisionWhenReady(this.labelRef.current);
147
+ }
118
148
  }
119
149
 
120
150
  componentWillUnmount() {
@@ -127,7 +157,11 @@ export class LatexLabel extends React.Component {
127
157
  // Fall back to the raw latex so a button is never blank.
128
158
  return markup ? (
129
159
  // eslint-disable-next-line react/no-danger
130
- <LabelHolder slanted={SLANTED_LABELS.includes(this.props.latex)} dangerouslySetInnerHTML={{ __html: markup }} />
160
+ <LabelHolder
161
+ ref={this.labelRef}
162
+ slanted={SLANTED_LABELS.includes(this.props.latex)}
163
+ dangerouslySetInnerHTML={{ __html: markup }}
164
+ />
131
165
  ) : (
132
166
  <LabelHolder slanted={SLANTED_LABELS.includes(this.props.latex)}>{this.props.latex}</LabelHolder>
133
167
  );
@@ -69,6 +69,18 @@ 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 smallest boxes are the ones in a subscript (`x_{}`, `\log_{}`) or exponent
73
+ // (`x^{}`): they sit at script size, so the 0.55em square reads cramped. They get
74
+ // 0.4em more height, and `\rule[-d]` decides which side it grows on:
75
+ // - subscript / log base: all of it goes below, the top edge stays put;
76
+ // - exponent: 0.3em goes above, plus 0.1em below.
77
+ const EMPTY_SUBSCRIPT_REGEX = /_\{\s*\}/g;
78
+ const EMPTY_SUPERSCRIPT_REGEX = /\^\{\s*\}/g;
79
+ const EMPTY_SUBSCRIPT_SLOT = '\\htmlData{pie-empty=1}{\\rule[-0.4em]{0.55em}{0.95em}}';
80
+ // The label clips what hangs below its line box (`overflow: hidden`), so the lowered
81
+ // box needs a zero-width strut after it to make room, or its bottom is cut off.
82
+ const EMPTY_SUBSCRIPT_STRUT = '\\rule[-0.5em]{0em}{0.5em}';
83
+ const EMPTY_SUPERSCRIPT_SLOT = '\\htmlData{pie-empty=1}{\\rule[-0.1em]{0.55em}{0.95em}}';
72
84
  // The segment key (`\overline{}`). The keypad centres every label in its cell, and
73
85
  // a thin bar is a much shorter label than the ray and line keys' stretchy arrows,
74
86
  // so by default both its bar and its box sit off from theirs: the bar is lower
@@ -111,6 +123,8 @@ const EMPTY_FENCE_SLOT = '\\rule{0em}{1em}\\htmlData{pie-empty=1}{\\rule[-0.25em
111
123
  export const withVisibleEmptySlots = (latex) =>
112
124
  (latex || '')
113
125
  .replace(EMPTY_OVERLINE_REGEX, `${EMPTY_OVERLINE_LABEL_STRUT}\\overline{${EMPTY_OVERLINE_BAR_STRUT}${EMPTY_SLOT}}`)
126
+ .replace(EMPTY_SUBSCRIPT_REGEX, `_{${EMPTY_SUBSCRIPT_SLOT}}${EMPTY_SUBSCRIPT_STRUT}`)
127
+ .replace(EMPTY_SUPERSCRIPT_REGEX, `^{${EMPTY_SUPERSCRIPT_SLOT}}`)
114
128
  .replace(EMPTY_GROUP_REGEX, `{${EMPTY_SLOT}}`)
115
129
  .replace(EMPTY_FENCE_REGEX, (_m, open, close) => `\\left${open}${EMPTY_FENCE_SLOT}\\right${close}`);
116
130
 
@@ -529,3 +543,7 @@ export const keyToAction = (key) => {
529
543
  };
530
544
 
531
545
  export { COMMANDS_WITH_ARGS, KEYSTROKES, MATHQUILL_ALIASES };
546
+
547
+ // TEMP: remove once confirmed that pie-elements picks up this build.
548
+ // eslint-disable-next-line no-console
549
+ console.log('[math-input-mathlive] latex-bridge loaded - taller sub/sup slots (PIE-1135 v2)');