@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/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
+ };
@@ -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;