@pie-lib/math-input-mathlive 1.1.5-beta.11 → 1.1.5-beta.12
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__/input-selection.test.js +103 -0
- package/lib/__tests__/keypad-label.test.js +13 -1
- package/lib/__tests__/latex-bridge.test.js +29 -1
- package/lib/__tests__/longdiv-wiring.test.js +215 -0
- package/lib/__tests__/longdiv.test.js +281 -0
- package/lib/__tests__/selection.test.js +195 -0
- package/lib/keypad/index.js +9 -1
- package/lib/keypad/index.js.map +1 -1
- package/lib/longdiv.js +173 -0
- package/lib/longdiv.js.map +1 -0
- package/lib/mathlive-instance.js +4 -0
- package/lib/mathlive-instance.js.map +1 -1
- package/lib/mf/input.js +8 -0
- package/lib/mf/input.js.map +1 -1
- package/lib/mf/static.js +13 -1
- package/lib/mf/static.js.map +1 -1
- package/lib/selection.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/longdiv-wiring.test.jsx +137 -0
- package/src/__tests__/longdiv.test.js +259 -0
- package/src/__tests__/selection.test.js +39 -4
- package/src/keypad/index.jsx +14 -2
- package/src/longdiv.js +172 -0
- package/src/mathlive-instance.js +4 -0
- package/src/mf/input.jsx +9 -0
- package/src/mf/static.jsx +15 -1
- package/src/selection.js +1 -6
package/src/longdiv.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
|
|
3
|
+
const log = debug('pie-lib:math-input-mathlive:longdiv');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Long division (`\enclose{longdiv}{...}`, which `\longdiv` is mapped to).
|
|
7
|
+
*
|
|
8
|
+
* MathLive 0.110 draws it wrong in three ways, all in `EncloseAtom.render`:
|
|
9
|
+
*
|
|
10
|
+
* 1. It lowers the dividend by `padding + clearance` (0.45em) with
|
|
11
|
+
* `position: relative; top`, which moves it off the baseline, so the divisor
|
|
12
|
+
* looks too high.
|
|
13
|
+
* 2. The notation box's CSS width leaves out the room it made for the `)`,
|
|
14
|
+
* while the SVG's viewBox includes it. The SVG is stretched to 100% of the
|
|
15
|
+
* box, so the whole drawing is scaled down (to about 86%).
|
|
16
|
+
* 3. The bar is drawn `base.width` long from the `)`, but the dividend itself
|
|
17
|
+
* starts after the `)` and its gap, so the bar stops short of the end of
|
|
18
|
+
* the dividend. `base.width` also leaves out the spacing MathLive puts
|
|
19
|
+
* around `,` and `+`, so even the right length would come up short.
|
|
20
|
+
*
|
|
21
|
+
* This reads back the numbers MathLive wrote into inline styles and the SVG,
|
|
22
|
+
* and corrects them, so the rest of MathLive's drawing (the arc, the stroke
|
|
23
|
+
* weight) is kept. The bar's end is measured from the rendered dividend when
|
|
24
|
+
* the markup is laid out, and computed from MathLive's numbers otherwise.
|
|
25
|
+
*
|
|
26
|
+
* Every value set here is absolute, so running it again is harmless - it has
|
|
27
|
+
* to run after every render of a live field anyway.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** A length MathLive wrote as `prop:<n>em` in an inline style, or 0. */
|
|
31
|
+
const inlineEm = (style, prop) => {
|
|
32
|
+
const match = new RegExp(`(?:^|;)\\s*${prop}:\\s*(-?[\\d.]+)em`).exec(style || '');
|
|
33
|
+
|
|
34
|
+
return match ? parseFloat(match[1]) : 0;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Direct child with a given tag. Not `querySelector(':scope > tag')`: jsdom's
|
|
39
|
+
* selector engine does not support `:scope` inside SVG elements.
|
|
40
|
+
*/
|
|
41
|
+
const childTag = (el, tag) => el && Array.from(el.children || []).find((c) => c.tagName.toLowerCase() === tag);
|
|
42
|
+
|
|
43
|
+
/** Right edge of what is actually drawn inside an element, ignoring the caret. */
|
|
44
|
+
const drawnRight = (el) => {
|
|
45
|
+
let right = -Infinity;
|
|
46
|
+
|
|
47
|
+
el.querySelectorAll('*').forEach((child) => {
|
|
48
|
+
if (child.children.length || (child.classList && child.classList.contains('ML__caret'))) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const rect = child.getBoundingClientRect();
|
|
53
|
+
|
|
54
|
+
if (rect.width > 0) {
|
|
55
|
+
right = Math.max(right, rect.right);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return right;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Correct every long division under `root`.
|
|
64
|
+
*
|
|
65
|
+
* @param {ParentNode} root a mathfield's shadow root, or an element holding
|
|
66
|
+
* static MathLive markup
|
|
67
|
+
* @returns {number} how many were corrected
|
|
68
|
+
*/
|
|
69
|
+
export const fixLongDivision = (root) => {
|
|
70
|
+
if (!root || typeof root.querySelectorAll !== 'function') {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let fixed = 0;
|
|
75
|
+
|
|
76
|
+
root.querySelectorAll('.ML__notation').forEach((notation) => {
|
|
77
|
+
try {
|
|
78
|
+
const svg = childTag(notation, 'svg');
|
|
79
|
+
const line = childTag(svg, 'line');
|
|
80
|
+
|
|
81
|
+
// Long division is the only notation MathLive draws with an arc (<path>).
|
|
82
|
+
if (!line || !childTag(svg, 'path')) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const dividend = notation.nextElementSibling;
|
|
87
|
+
const style = dividend && dividend.getAttribute('style');
|
|
88
|
+
const width = inlineEm(style, 'width');
|
|
89
|
+
const gap = inlineEm(style, 'margin-left');
|
|
90
|
+
const drop = inlineEm(style, 'top');
|
|
91
|
+
const viewBox = (svg.getAttribute('viewBox') || '').trim().split(/\s+/).map(Number);
|
|
92
|
+
const start = parseFloat(line.getAttribute('x1'));
|
|
93
|
+
|
|
94
|
+
if (!width || viewBox.length !== 4 || Number.isNaN(start)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 2. size the box to the viewBox, so the drawing is not scaled down
|
|
99
|
+
notation.style.width = `calc(${viewBox[2]}em + 1px)`;
|
|
100
|
+
|
|
101
|
+
// 1. put the dividend back on the baseline. Growing the box upwards by the
|
|
102
|
+
// same amount keeps the drawing where it was relative to the dividend, and
|
|
103
|
+
// makes the line tall enough that the field does not clip the bar.
|
|
104
|
+
if (drop && notation.parentElement) {
|
|
105
|
+
dividend.style.top = '0';
|
|
106
|
+
notation.parentElement.style.paddingTop = `${drop}em`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 3. run the bar from the ")" to the end of the dividend
|
|
110
|
+
let end = start + gap + width;
|
|
111
|
+
const svgRect = svg.getBoundingClientRect();
|
|
112
|
+
const right = drawnRight(dividend);
|
|
113
|
+
|
|
114
|
+
if (svgRect.width > 0 && Number.isFinite(right)) {
|
|
115
|
+
end = (right - svgRect.left) * (viewBox[2] / svgRect.width);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
line.setAttribute('x2', String(end));
|
|
119
|
+
fixed += 1;
|
|
120
|
+
} catch (e) {
|
|
121
|
+
log('could not correct a long division: %s', e && e.message);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return fixed;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Correct static markup now, and again once web fonts have loaded: the bar's
|
|
130
|
+
* end is measured, and glyph widths change when the math fonts arrive.
|
|
131
|
+
*
|
|
132
|
+
* @param {Element} element
|
|
133
|
+
*/
|
|
134
|
+
export const fixLongDivisionWhenReady = (element) => {
|
|
135
|
+
fixLongDivision(element);
|
|
136
|
+
|
|
137
|
+
if (typeof document !== 'undefined' && document.fonts && document.fonts.ready) {
|
|
138
|
+
document.fonts.ready.then(() => fixLongDivision(element)).catch(() => {});
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Keep a live mathfield's long divisions corrected. MathLive rebuilds its shadow
|
|
144
|
+
* DOM on every render (each keystroke, a resize, the fonts loading), which
|
|
145
|
+
* discards the corrections, so they are reapplied whenever it does.
|
|
146
|
+
*
|
|
147
|
+
* @param {HTMLElement} mathField a MathLive `<math-field>`
|
|
148
|
+
* @returns {function|undefined} teardown, to call on unmount
|
|
149
|
+
*/
|
|
150
|
+
export const trackLongDivision = (mathField) => {
|
|
151
|
+
const root = mathField && mathField.shadowRoot;
|
|
152
|
+
|
|
153
|
+
if (!root) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const run = () => fixLongDivision(root);
|
|
158
|
+
|
|
159
|
+
run();
|
|
160
|
+
|
|
161
|
+
if (typeof MutationObserver === 'undefined') {
|
|
162
|
+
return () => {};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Only childList: the corrections themselves change attributes, so they do
|
|
166
|
+
// not retrigger this.
|
|
167
|
+
const observer = new MutationObserver(run);
|
|
168
|
+
|
|
169
|
+
observer.observe(root, { childList: true, subtree: true });
|
|
170
|
+
|
|
171
|
+
return () => observer.disconnect();
|
|
172
|
+
};
|
package/src/mathlive-instance.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import debug from 'debug';
|
|
2
2
|
import { NEWLINE_EMBED, toMathLive, withVisibleEmptySlots } from './latex-bridge';
|
|
3
3
|
import { ROOT_INDEX_CSS, PARALLEL_CSS } from './mf/common-styles';
|
|
4
|
+
import { fixLongDivisionWhenReady } from './longdiv';
|
|
4
5
|
|
|
5
6
|
const log = debug('pie-lib:math-input-mathlive:instance');
|
|
6
7
|
|
|
@@ -545,6 +546,9 @@ export function applyStaticMath(element, latex, options = {}) {
|
|
|
545
546
|
macros: { ...getMacros(), ...(options.macros || {}) },
|
|
546
547
|
});
|
|
547
548
|
|
|
549
|
+
// MathLive misdraws long division (see longdiv.js).
|
|
550
|
+
fixLongDivisionWhenReady(element);
|
|
551
|
+
|
|
548
552
|
return element;
|
|
549
553
|
}
|
|
550
554
|
|
package/src/mf/input.jsx
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '../mathlive-instance';
|
|
12
12
|
import { toMathLive, fromMathLive, keyToAction, KEYSTROKES } from '../latex-bridge';
|
|
13
13
|
import { trackWholeAtomSelection } from '../selection';
|
|
14
|
+
import { trackLongDivision } from '../longdiv';
|
|
14
15
|
|
|
15
16
|
const log = debug('pie-lib:math-input-mathlive:input');
|
|
16
17
|
|
|
@@ -97,6 +98,9 @@ export class Input extends React.Component {
|
|
|
97
98
|
// Shift+Arrow would otherwise select half a fraction; snap to whole atoms.
|
|
98
99
|
this.untrackSelection = trackWholeAtomSelection(this.mathField);
|
|
99
100
|
|
|
101
|
+
// MathLive misdraws long division; correct it after every render.
|
|
102
|
+
this.untrackLongDivision = trackLongDivision(this.mathField);
|
|
103
|
+
|
|
100
104
|
this.mathField.addEventListener('input', this.onInputEdit);
|
|
101
105
|
|
|
102
106
|
// Belt and braces: drop anything a previous mount may have left behind.
|
|
@@ -145,6 +149,11 @@ export class Input extends React.Component {
|
|
|
145
149
|
this.untrackSelection = undefined;
|
|
146
150
|
}
|
|
147
151
|
|
|
152
|
+
if (this.untrackLongDivision) {
|
|
153
|
+
this.untrackLongDivision();
|
|
154
|
+
this.untrackLongDivision = undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
148
157
|
if (this.mathField) {
|
|
149
158
|
this.mathField.removeEventListener('input', this.onInputEdit);
|
|
150
159
|
this.mathField.remove();
|
package/src/mf/static.jsx
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
applyShadowStyles,
|
|
11
11
|
trackPlaceholderCaret,
|
|
12
12
|
} from '../mathlive-instance';
|
|
13
|
+
import { trackLongDivision, fixLongDivisionWhenReady } from '../longdiv';
|
|
13
14
|
import { toMathLive, fromMathLive, fieldIds } from '../latex-bridge';
|
|
14
15
|
import { placeholderStyles, rootIndexStyles, parallelStyles } from './common-styles';
|
|
15
16
|
|
|
@@ -114,6 +115,9 @@ export default class Static extends React.Component {
|
|
|
114
115
|
// blocks are typed into with the same keypad, so they grow placeholders too.
|
|
115
116
|
this.untrackCaret = trackPlaceholderCaret(this.mathField);
|
|
116
117
|
|
|
118
|
+
// MathLive misdraws long division; correct it after every render.
|
|
119
|
+
this.untrackLongDivision = trackLongDivision(this.mathField);
|
|
120
|
+
|
|
117
121
|
this.mathField.addEventListener('input', this.onPromptInput);
|
|
118
122
|
this.mathField.addEventListener('focusin', this.onPromptFocus);
|
|
119
123
|
this.mathField.addEventListener('keydown', this.onKeyDown);
|
|
@@ -124,7 +128,12 @@ export default class Static extends React.Component {
|
|
|
124
128
|
this.createLiveRegion();
|
|
125
129
|
}
|
|
126
130
|
|
|
127
|
-
componentDidUpdate(prevProps) {
|
|
131
|
+
componentDidUpdate(prevProps, prevState) {
|
|
132
|
+
// Display mode: new markup was just written into the DOM.
|
|
133
|
+
if (!this.mathField && this.state.markup !== (prevState && prevState.markup)) {
|
|
134
|
+
fixLongDivisionWhenReady(this.holderRef.current);
|
|
135
|
+
}
|
|
136
|
+
|
|
128
137
|
if (prevProps.latex === this.props.latex) {
|
|
129
138
|
return;
|
|
130
139
|
}
|
|
@@ -145,6 +154,11 @@ export default class Static extends React.Component {
|
|
|
145
154
|
this.untrackCaret = undefined;
|
|
146
155
|
}
|
|
147
156
|
|
|
157
|
+
if (this.untrackLongDivision) {
|
|
158
|
+
this.untrackLongDivision();
|
|
159
|
+
this.untrackLongDivision = undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
148
162
|
if (this.mathField) {
|
|
149
163
|
this.mathField.removeEventListener('input', this.onPromptInput);
|
|
150
164
|
this.mathField.removeEventListener('focusin', this.onPromptFocus);
|
package/src/selection.js
CHANGED
|
@@ -120,12 +120,7 @@ export const trackWholeAtomSelection = (mathField) => {
|
|
|
120
120
|
const [start, end] = range;
|
|
121
121
|
const lo = Math.min(start, end);
|
|
122
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
|
-
);
|
|
123
|
+
const [nextStart, nextEnd] = expandRange(lo, hi, (i) => mathField.getElementInfo(i), mathField.lastOffset);
|
|
129
124
|
|
|
130
125
|
if (nextStart === lo && nextEnd === hi) {
|
|
131
126
|
return;
|