@pie-lib/math-input-mathlive 1.0.9 → 1.0.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.
@@ -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 an actual glyph is required.
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}{\\unicode{"25AB}}';
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
  /**
@@ -240,8 +248,15 @@ export const fromMathLive = (latex) => {
240
248
  * placeholders in the inserted latex.
241
249
  */
242
250
  const COMMANDS_WITH_ARGS = {
251
+ // An explicit "blank over blank" key: both slots start empty.
243
252
  '\\frac': '\\frac{#?}{#?}',
244
- '/': '\\frac{#?}{#?}',
253
+ // Division, which must behave like MathQuill's `/`: whatever precedes the
254
+ // cursor becomes the numerator and the caret lands in the denominator. `#@`
255
+ // is MathLive's token for "the selection, or the item before the cursor" -
256
+ // this is exactly the template MathLive binds to the `/` key itself. Using
257
+ // `#?` here instead produced an empty fraction and left the typed number
258
+ // stranded outside it.
259
+ '/': '\\frac{#@}{#?}',
245
260
  '\\sqrt': '\\sqrt{#?}',
246
261
  '\\nthroot': '\\sqrt[#?]{#?}',
247
262
  '\\overline': '\\overline{#?}',
@@ -180,6 +180,67 @@ const configureLoaded = () => {
180
180
  injectCoreStylesheet();
181
181
  };
182
182
 
183
+ /**
184
+ * CSS that has to live *inside* a mathfield's shadow root.
185
+ *
186
+ * A live `<math-field>` renders in shadow DOM (`attachShadow({mode:'open'})`),
187
+ * so nothing in the host page's stylesheets reaches it. The stretchy-accent
188
+ * problem therefore needs fixing twice: once for static markup (see
189
+ * `placeholderStyles`) and once here for the editor.
190
+ *
191
+ * MathLive centres an accent with `margin-left: <half the content width>` as an
192
+ * inline style. For a stretchy accent that also shrinks its containing block
193
+ * (`.ML__center` is position:relative), so the arc ends up half-width and offset
194
+ * - visibly failing to span the text underneath.
195
+ *
196
+ * `:has(.ML__stretchy)` keeps the fix to stretchy accents; single-glyph accents
197
+ * (`\hat{x}`, `\vec{x}`) still need their centring offset.
198
+ */
199
+ const SHADOW_CSS = '.ML__latex .ML__center:has(.ML__stretchy){margin-left:0 !important}';
200
+
201
+ /**
202
+ * Apply {@link SHADOW_CSS} inside a mathfield's shadow root.
203
+ *
204
+ * Uses `adoptedStyleSheets` when available (which is how MathLive loads its own
205
+ * stylesheets) and falls back to appending a <style> node.
206
+ *
207
+ * @param {HTMLElement} mathField
208
+ */
209
+ export const applyShadowStyles = (mathField) => {
210
+ const root = mathField && mathField.shadowRoot;
211
+
212
+ if (!root) {
213
+ return;
214
+ }
215
+
216
+ try {
217
+ if ('adoptedStyleSheets' in root && typeof CSSStyleSheet !== 'undefined') {
218
+ // Do not re-add on a re-render.
219
+ if (root.__pieAccentSheet) {
220
+ return;
221
+ }
222
+
223
+ const sheet = new CSSStyleSheet();
224
+
225
+ sheet.replaceSync(SHADOW_CSS);
226
+ root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
227
+ root.__pieAccentSheet = sheet;
228
+
229
+ return;
230
+ }
231
+
232
+ if (!root.querySelector('style[data-pie-accent]')) {
233
+ const style = document.createElement('style');
234
+
235
+ style.setAttribute('data-pie-accent', '');
236
+ style.textContent = SHADOW_CSS;
237
+ root.appendChild(style);
238
+ }
239
+ } catch (e) {
240
+ log('could not apply shadow styles to the mathfield: %s', e && e.message);
241
+ }
242
+ };
243
+
183
244
  /**
184
245
  * Ensure MathLive's "core" stylesheet is in the document.
185
246
  *
@@ -57,13 +57,42 @@ export const placeholderStyles = {
57
57
  '& [data-pie-empty]': {
58
58
  opacity: 0.5,
59
59
  },
60
- // Stretchy accents render an <svg>; mathlive-static.css positions it
61
- // absolutely at width:100%. Repeated here so static math cannot blow out its
62
- // container if that stylesheet is missing.
63
- '& svg': {
60
+ // Stretchy accents (\overarc, \overleftrightarrow, \overrightarrow) render an
61
+ // <svg width="100%" preserveAspectRatio="none"> inside a `.ML__stretchy`. That
62
+ // span is absolutely positioned, so its immediate wrapper computes to 0px and
63
+ // the width has to resolve against a *positioned* ancestor. Pin that to
64
+ // `.ML__base`, whose width already equals the accent's content (measured:
65
+ // 9.85px for a 0.56em slot), so the arc spans exactly its content.
66
+ //
67
+ // Do NOT write a bare `& svg` rule instead: MathLive's own rule is
68
+ // descendant-scoped (`.ML__latex .ML__stretchy svg`), and a global one
69
+ // re-parents every svg in the subtree.
70
+ '& .ML__base': {
71
+ position: 'relative',
72
+ },
73
+ '& .ML__stretchy': {
64
74
  position: 'absolute',
75
+ left: 0,
65
76
  width: '100%',
66
77
  },
78
+ // MathLive centres an accent by shifting it right half the content width -
79
+ // `margin-left: 0.28em` for a 0.56em slot - which assumes a glyph with its own
80
+ // side bearings. Over a synthetic empty slot it just pushes the arc right.
81
+ //
82
+ // `!important` is required: MathLive writes this as an INLINE style
83
+ // (`style="top:-3.4em;margin-left:0.28em"`), which a class cannot override.
84
+ //
85
+ // It also fixes the width. `.ML__center` is the stretchy's containing block
86
+ // (MathLive gives `.ML__vlist > span` position:relative), and its width is the
87
+ // base minus that margin - measured 9.85px - 4.93px = 4.93px, exactly half,
88
+ // which is why the arc came out half-width AND offset right.
89
+ //
90
+ // Scoped with `:has(.ML__stretchy)`: only stretchy accents are affected.
91
+ // A single-glyph accent (`\hat{x}`, `\vec{x}`) genuinely needs its centring
92
+ // offset, and must keep it.
93
+ '& .ML__center:has(.ML__stretchy)': {
94
+ marginLeft: '0 !important',
95
+ },
67
96
  };
68
97
 
69
98
  /**
package/src/mf/input.jsx CHANGED
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { styled } from '@mui/material/styles';
4
4
  import debug from 'debug';
5
- import { loadMathLive, getMacros, MATH_MODE_SPACE } from '../mathlive-instance';
5
+ import { loadMathLive, getMacros, MATH_MODE_SPACE, applyShadowStyles } from '../mathlive-instance';
6
6
  import { toMathLive, fromMathLive, keyToAction, KEYSTROKES } from '../latex-bridge';
7
7
 
8
8
  const log = debug('pie-lib:math-input-mathlive:input');
@@ -75,6 +75,10 @@ export class Input extends React.Component {
75
75
  // defaults to an empty string.
76
76
  this.mathField.mathModeSpace = MATH_MODE_SPACE;
77
77
 
78
+ // The mathfield renders in shadow DOM, so page CSS cannot reach it - the
79
+ // stretchy-accent fix has to be injected there directly.
80
+ applyShadowStyles(this.mathField);
81
+
78
82
  this.mathField.addEventListener('input', this.onInputEdit);
79
83
 
80
84
  // Belt and braces: drop anything a previous mount may have left behind.
package/src/mf/static.jsx CHANGED
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { styled } from '@mui/material/styles';
4
4
  import debug from 'debug';
5
- import { loadMathLive, getMacros, latexToMarkup, MATH_MODE_SPACE } from '../mathlive-instance';
5
+ import { loadMathLive, getMacros, latexToMarkup, MATH_MODE_SPACE, applyShadowStyles } from '../mathlive-instance';
6
6
  import { toMathLive, fromMathLive, fieldIds } from '../latex-bridge';
7
7
  import { placeholderStyles } from './common-styles';
8
8
 
@@ -93,6 +93,10 @@ export default class Static extends React.Component {
93
93
  this.mathField.mathModeSpace = MATH_MODE_SPACE;
94
94
  this.mathField.value = toMathLive(this.props.latex);
95
95
 
96
+ // The mathfield renders in shadow DOM, so page CSS cannot reach it - the
97
+ // stretchy-accent fix has to be injected there directly.
98
+ applyShadowStyles(this.mathField);
99
+
96
100
  this.mathField.addEventListener('input', this.onPromptInput);
97
101
  this.mathField.addEventListener('focusin', this.onPromptFocus);
98
102
  this.mathField.addEventListener('keydown', this.onKeyDown);